Skip to main content
A provider plugin adds a service Enconvo already has a picker for. Once it is installed, its AI model shows up in the model picker next to OpenAI, Anthropic and the rest, and its voice shows up in the text-to-speech voice list. Chats, agents, read-aloud and workflows then use it exactly like a built-in provider. This page builds one plugin, acme_ai, with both kinds: a chat model for an OpenAI-compatible API and a text-to-speech voice. It assumes you have read Developing Extensions.
Provider plugins need Enconvo 2.5.6 or later. Set "minAppVersion": "2.5.6" in package.json.

How a provider plugin fits in

Start from the scaffold (npx -p @enconvo/api enconvo plugin create acme_ai), then replace its sample command with the files below.
  • Each provider is a command with "commandType": "provider" and a provider_category: llm for AI models, tts for voices.
  • Its source file exports main(options), which returns an instance of the SDK base class (LLMProvider or TTSProvider). Enconvo creates the instance and calls it; the plugin never runs as a standalone command.
  • Enconvo names a plugin’s provider by its full command key, acme_ai|chat, so it can never clash with a built-in one such as llm|open_ai.
  • Installing a plugin never makes its provider the default. Users choose it in the picker. If the plugin is uninstalled, anything that had it selected goes back to the default.

The manifest

Keep the scripts, dependencies and devDependencies that enconvo plugin create wrote. What matters here:
  • "private": true keeps the provider out of the command list; it is only reached through the pickers.
  • The preference names are fixed: an AI model provider reads its model from modelName, and a voice provider reads its voice from voice. The pickers write to those names.
  • dataProxy points at the plugin’s own API route (acme_ai/models). The default is the value of one item in that list. At run time Enconvo hands the provider the whole item, so the code reads this.options.modelName.value.
  • A preference at the top level of the manifest, like api_key, is shared by every command and route in the plugin. Users fill it in on the plugin’s page in Settings, and the provider reads it as this.options.api_key. Use "type": "password" for keys so they are stored encrypted.
  • isDefault and sort are removed when a plugin is installed; a plugin can’t put its provider first or make it the default.
  • The routes in src/api/ need no entry in commands; the build adds them.

The model and voice lists

The picker calls the dataProxy route whenever it opens, so the route must answer fast. The simplest route returns a fixed list. src/api/models.ts:
Each model item has: To list the models your API offers instead, fetch them inside ListCache. It keeps the list until the user refreshes it in the picker, and the plugin’s preferences, such as api_key, arrive in the options:
src/api/voices.ts:
@private keeps these routes out of the plugin’s generated API docs, since only the pickers call them.

An AI model provider

Extend LLMProvider and implement two methods:
  • _call(params) returns the whole reply as an AssistantMessage.
  • _stream(params) returns a Stream of reply events as they arrive. Chats use this one.
src/chat.ts, for an API that follows OpenAI’s chat completions format:

The stream events

_stream does not pass your API’s chunks through. It yields Enconvo’s content-block events, which follow Anthropic’s streaming format: Close each block before you start the next one. Stream.fromSSEResponse reads a server-sent events response and skips OpenAI’s [DONE] line; for any other format, parse the response yourself inside the generator. Abort the controller when params.signal fires, so that the Stop button ends the request.

Messages

params.messages is the conversation, already trimmed to the model’s context window. Each message has a role (system, user, assistant or tool) and a content list of parts: text, image_url, file, and flow_step for earlier tool calls. The example keeps only the text, which is enough for a model with toolUse: false and visionEnable: false. To offer images, set visionEnable: true on the model and send the image_url parts in your API’s image format. To offer tools, set toolUse: true and send params.tools to your API. Stream each call back as a tool_use block: content_block_start with { type: "tool_use", id, name, input: {} }, then content_block_delta events with { type: "input_json_delta", partial_json }. Turn the flow_step parts of earlier messages back into your API’s tool call and tool result messages. Enconvo runs the tools and calls the model again with the results.

A text-to-speech provider

Extend TTSProvider and implement _toFile. It gets the text and the path to write, and returns the path once the audio is there. src/say.ts:
The TTSProvider constructor takes { options }, while LLMProvider takes the options themselves.
  • voice is the value of the voice the user picked.
  • format is the file type to write, mp3 unless the caller asks for another. audioFilePath already ends in it.
  • speed is a number, 1 for normal speed. Send it on if your API supports it.
  • If your provider returns a path to an empty or missing file, Enconvo reports an error instead of playing silence.

Try it and publish it

npm run dev builds the plugin into Enconvo and rebuilds it whenever you save.
  1. Open the plugin’s page in Enconvo’s Settings and enter the API key.
  2. Open the model picker in a chat. Acme AI is listed with the other providers; pick a model and send a message. The reply streams in as your _stream yields it.
  3. In the text-to-speech settings, pick Acme AI Voice and play a voice preview, or select some text and read it aloud.
If something fails, the error your provider throws is shown in the chat or the preview, so put the API’s status and message in it, as the examples do. When it works, publish it like any other plugin:
The Plugin Development Guide plugin in Enconvo’s plugin store carries this guide as a skill, so an agent in Enconvo can help you build a provider plugin.