How the Plugins System in NextChat Works: OpenAPI to Function Tools

NextChat's plugins system treats every plugin as an OpenAPI-described HTTP service, dynamically converting API operations into LLM function tools that authenticate and invoke external services during chat sessions.

NextChat (formerly ChatGPTNextWeb) implements a flexible plugins architecture that bridges OpenAPI specifications with LLM function calling. The system loads plugin definitions from JSON manifests located in public/plugins.json, parses OpenAPI schemas to generate callable tools via the FunctionToolService, and manages authentication injection at request time. This allows users to extend chat capabilities with any HTTP API that exposes a valid OpenAPI description.

Architecture Overview

The plugins system consists of four core components working together to transform static API specifications into runtime function tools:

  • public/plugins.json – The built-in plugin catalogue containing IDs, display names, and URLs to OpenAPI schemas
  • app/store/plugin.ts – The central Zustand store that persists plugin data and hosts the FunctionToolService class
  • app/components/plugin.tsx – The React UI for creating, editing, and managing plugin instances
  • app/constant.ts – Defines PLUGINS_REPO_URL, the external repository pointer for fetching built-in plugin schemas

When a user enables plugins for a conversation, the store's getAsTools() method returns both the function descriptors (for the LLM) and the executable wrappers (for the runtime).

Loading Built-in Plugins

On application startup, the usePluginStore triggers its onRehydrateStorage lifecycle hook to populate the default plugin registry:

  1. Fetch Manifest – Downloads ./plugins.json to retrieve the list of official plugins
  2. Download Schemas – For each entry, retrieves the OpenAPI JSON/YAML from the schema URL
  3. Create Records – Invokes state.create(item) to instantiate Plugin objects with randomized nanoId identifiers
  4. Register Tools – Calls state.updatePlugin(..., add(plugin, true)) to parse the OpenAPI spec and register operations in FunctionToolService
  5. Mark Built-in – Copies the OpenAPI title/version into the plugin record and sets the builtin flag to true

This process ensures that official plugins like DALL-E 3 or web search are immediately available without user configuration.

The FunctionToolService Engine

The FunctionToolService class, defined within app/store/plugin.ts, serves as the transformation engine. Its add(plugin, replace) method performs the heavy lifting of converting OpenAPI specifications into executable JavaScript functions.

Transforming OpenAPI to Function Tools

When add() receives a plugin object, it executes the following transformation pipeline:

const definition = yaml.load(plugin.content) as any;
const serverURL = definition?.servers?.[0]?.url;
const baseURL = !isApp ? "/api/proxy" : serverURL;
const headers = { "X-Base-URL": !isApp ? serverURL : undefined };
if (authLocation === "header") headers[headerName] = tokenValue;

const api = new OpenAPIClientAxios({ definition });
await api.init();

Operation Mapping – The service iterates through api.getOperations() to build two critical data structures:

  • FunctionToolItem array – Contains function descriptors with:

    • name: Derived from operationId or generated from path/method
    • description: Extracted from OpenAPI description or summary fields
    • parameters: Merged JSON schema combining request body, query, and path parameters
  • funcs map – Contains executable wrappers that assemble the final HTTP request, inject authentication tokens based on authLocation, and forward to api.client.paths[path][method]

These structures are cached in FunctionToolService.tools[plugin.id] for rapid retrieval during chat sessions.

Authentication Handling

The engine supports three authentication injection strategies stored in the Plugin object:

  • authLocation: "header" – Injects authToken into the specified authHeader (e.g., Authorization)
  • authLocation: "query" – Appends the token as a query parameter using the authHeader as the key name
  • authLocation: "body" – Merges the token into the request payload

This configuration is applied at request time within the wrapper functions, ensuring sensitive credentials never leak into the stored OpenAPI schema.

Adding Custom Plugins

Users can extend NextChat with custom APIs through either programmatic interfaces or the management UI.

Programmatic Creation

Developers can dynamically register plugins using the store methods:

import { usePluginStore } from '@/store/plugin';

const store = usePluginStore();

// Create plugin record
const plugin = store.create({
  id: 'weather-api',
  title: 'WeatherService',
  version: '1.0.0',
  content: `
openapi: 3.0.0
info:
  title: WeatherService
  version: 1.0.0
servers:
  - url: https://api.weather.com/v1
paths:
  /current:
    get:
      operationId: getCurrentWeather
      description: Retrieve current weather conditions
      parameters:
        - in: query
          name: city
          schema:
            type: string
          required: true
      responses:
        '200':
          description: Success
`
});

// Register with optional authentication
store.updatePlugin(plugin.id, p => {
  p.authType = 'bearer';
  p.authToken = 'sk-weather-token';
  p.authLocation = 'header';
  p.authHeader = 'Authorization';
});

The create() method generates a unique identifier and stores the raw OpenAPI content, while updatePlugin() triggers the parsing logic in FunctionToolService.add().

UI-Based Management

The PluginPage component in app/components/plugin.tsx provides a visual editor bound to the plugin's content field. When the user modifies the OpenAPI schema:

  1. The UI attempts yaml.load() validation
  2. Instantiates a temporary OpenAPIClientAxios client to verify the definition
  3. Calls pluginStore.updatePlugin() to rebuild tool descriptors via FunctionToolService.add(plugin, true)
  4. Persists authentication settings (authType, authLocation, etc.) to the store

Runtime Execution Flow

When a conversation requires plugin capabilities, the application follows this execution sequence:

1. Tool Retrieval

const [tools, funcs] = pluginStore.getAsTools(selectedPluginIds);

The getAsTools() method aggregates all FunctionToolItem arrays and funcs maps from the requested plugin IDs into unified collections.

2. LLM Submission

The tools array is serialized into the functions parameter of the OpenAI or Claude API request:

await fetch('/api/openai/chat', {
  method: 'POST',
  body: JSON.stringify({
    messages: conversationHistory,
    functions: tools,
    function_call: "auto"
  })
});

3. Function Invocation

When the model responds with a function_call object containing name and arguments, the frontend:

  1. Parses the JSON arguments
  2. Looks up the implementation: const handler = funcs[functionCall.name]
  3. Executes the HTTP request: const result = await handler(parsedArgs)
  4. Appends the result as a function role message to the conversation history

This round-trip allows the LLM to retrieve real-time data, generate images, or trigger external workflows while maintaining conversational context.

Summary

  • OpenAPI-Native: NextChat plugins require valid OpenAPI 3.0 specifications, with each operation automatically converted to a callable function tool
  • Centralized Service: The FunctionToolService class in app/store/plugin.ts handles parsing, caching, and request execution for all plugins
  • Flexible Auth: Supports header, query, and body-based authentication injection configured per-plugin via the UI or programmatically
  • Built-in Registry: Default plugins load from public/plugins.json and external repositories defined in app/constant.ts
  • Runtime Integration: The getAsTools() method bridges the plugin store with LLM function calling, returning both descriptors for the model and executable wrappers for the client

Frequently Asked Questions

What format must plugin schemas follow?

Plugins must provide valid OpenAPI 3.0 specifications in either JSON or YAML format. The FunctionToolService uses yaml.load() for parsing, so YAML is preferred for readability. Each operation requires an operationId (or generates one from the path/method) to serve as the function name exposed to the LLM.

How does authentication work for external APIs?

Authentication is handled at the plugin level through the authType, authLocation, authHeader, and authToken fields stored in the Plugin object. During request execution, FunctionToolService.add() injects these credentials into headers, query parameters, or the request body based on the authLocation setting, ensuring tokens are never hardcoded in the OpenAPI schema itself.

Can I use custom plugins not in the official repository?

Yes. Users can create custom plugins through the PluginPage UI or programmatically via usePluginStore().create(). Custom plugins follow the same OpenAPI-to-function transformation pipeline as built-in plugins, allowing integration with private internal APIs or third-party services not included in the default plugins.json manifest.

Where does the actual HTTP request execute?

In web deployments, requests route through /api/proxy with the target URL passed in the X-Base-URL header to avoid CORS issues. In native app builds (isApp === true), requests execute directly against the serverURL defined in the OpenAPI spec's servers array. The FunctionToolService automatically determines the appropriate base URL and request strategy based on the environment.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →