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
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 aprovider_category:llmfor AI models,ttsfor voices. - Its source file exports
main(options), which returns an instance of the SDK base class (LLMProviderorTTSProvider). 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 asllm|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
scripts, dependencies and devDependencies that enconvo plugin create wrote. What matters here:
"private": truekeeps 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 fromvoice. The pickers write to those names. dataProxypoints at the plugin’s own API route (acme_ai/models). Thedefaultis thevalueof one item in that list. At run time Enconvo hands the provider the whole item, so the code readsthis.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 asthis.options.api_key. Use"type": "password"for keys so they are stored encrypted. isDefaultandsortare 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 incommands; the build adds them.
The model and voice lists
The picker calls thedataProxy route whenever it opens, so the route must answer fast. The simplest route returns a fixed list.
src/api/models.ts:
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
ExtendLLMProvider and implement two methods:
_call(params)returns the whole reply as anAssistantMessage._stream(params)returns aStreamof 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
ExtendTTSProvider and implement _toFile. It gets the text and the path to write, and returns the path once the audio is there.
src/say.ts:
TTSProvider constructor takes { options }, while LLMProvider takes the options themselves.
voiceis thevalueof the voice the user picked.formatis the file type to write,mp3unless the caller asks for another.audioFilePathalready ends in it.speedis 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.
- Open the plugin’s page in Enconvo’s Settings and enter the API key.
- 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
_streamyields it. - In the text-to-speech settings, pick Acme AI Voice and play a voice preview, or select some text and read it aloud.