A SoAI plugin is a .soaiplugin ZIP archive whose root __init__.py defines one direct
Plugin class derived from plugin_sdk.BasePlugin.
Plugin architecture describes how the host inspects, prepares, and runs
that package, and Plugin SDK reference documents the import surface it may
use, so this page is the build path: how to lay out the archive, what the manifest has to declare literally, which
methods you must implement, and how to get the result loaded and tested. Work in a source directory outside the
plugins folder and package from there. Review License and legal for the terms that apply
to independently written plugins and Core SDK files, and read
Security hardening before installing a package from another source, because a
worker runs with the permissions of the operating-system account that runs SoAI.
If you need a reusable assistant workflow rather than an inference engine or provider integration, follow Creating a skill. That guide also includes an AI coding-agent prompt with a task brief, package requirements, and acceptance checks for a skill.
Lay out the archive
my_plugin_source/
__init__.py
request_adapter.py
model_parameters.json
LICENSES/
LICENSE_SOAIPLUGIN.txt
my_plugin.soaiplugin
__init__.py
request_adapter.py
model_parameters.json
LICENSES/
LICENSE_SOAIPLUGIN.txt
Archive the contents of the source directory rather than the directory itself, keep the entrypoint at the root, and
put cohesive modules beside it with relative imports. The bundled packages under plugins/ are ordinary
ZIP archives you can open as a working reference for a specific backend type. Every source member must decode as UTF-8
and parse, and inspection rejects bytecode and __pycache__ along with an archive carrying more than 512
source members, a single member over 8 MiB, or more than 64 MiB of source in total. An optional root
model_parameters.json must be a JSON object that follows the model-parameter schema and stay under 8
MiB. Keeping credentials, local model files, plugin_sdk itself, and the generated
.soaiplugin.yaml sidecar out of the archive is author discipline rather than a validated rule, so
nothing will reject a package that ships them; SoAI writes the sidecar itself from DEFAULT_CONFIGURATION
and keeps it next to the archive.
A plugin package may include one optional, case-sensitive root logo.png or logo.webp file; it must be a static PNG or WebP image no larger than 1 MiB and 512 pixels on either side. SoAI sanitizes valid artwork for automatic display in plugin and model catalogs, and uses the existing built-in or generic artwork when the logo is absent or invalid.
The archive filename decides the installed identity. SoAI takes the file stem, which must be alphanumerics, dashes,
or underscores and must not begin with an underscore, and uses it as the plugin ID throughout the catalog, while
NAME is only the display name. Installed packages live in the directory named by
PLUGINS.PATHS.PLUGINS, which resolves to
plugins inside the SoAI directory unless you change it.
Declare the manifest as literals
SoAI reads the manifest by parsing the entrypoint before the package ever runs, so every required value must be a
plain literal assigned directly on Plugin. A computed expression, a property, an inherited default, or
an imported constant is rejected even when it would produce the correct value at runtime, and an annotation is fine,
since NAME: str = "Acme" and MODEL_TYPES: typing.ClassVar[list[str]] = ["acme"] both read
as literal assignments. EXTERNAL_PROVIDER_MODE is the one exception and may be written as a
plugin_sdk.ExternalProviderMode member, a string, or None.
| Group | Required literal assignments |
|---|---|
| Identity and licensing | NAME, VERSION_SOAIPLUGIN, AUTHOR_SOAIPLUGIN, DESCRIPTION_SOAIPLUGIN, WEBSITE_SOAIPLUGIN, WEBSITE_BACKEND, and LICENSE_SOAIPLUGIN. |
| Compatibility and dependencies | REQUIRED_SOAI_VERSION, PLUGIN_DEPENDENCIES, PACKAGE_DEPENDENCIES, and REQUIRED_SYSTEM_CAPABILITIES. |
| Models and resources | MODEL_REPOSITORY, MODEL_TYPES, ALIASES, SUPPORTED_MODALITIES, LOCAL_RESOURCES, and LOCAL_MODELS. |
| Runtime and management | PERSISTENT, MAX_CONCURRENT_REQUESTS, SUPPORTS_BACKEND_INSTALLATION, SUPPORTS_BACKEND_PROCESS_TRACKING, SUPPORTS_MODEL_DELETION, SUPPORTS_MODEL_DOWNLOAD, SUPPORTS_CONFIGURATION, SUPPORTS_EXTERNAL_PROVIDERS, EXTERNAL_PROVIDER_MODE, and SUPPORTS_CLONING. |
Every flag in OPENAI_FEATURE_MATRIX is required as a literal boolean too. The matrix
covers Chat Completions, Completions, Responses and its subresources, embeddings, images, audio endpoints, image
edits and variations, vision, input audio, tool calling, parallel tool calls, structured output, and JSON Schema,
and it is exported through the top-level SDK. The worker repeats that check against the loaded class and adds
SUPPORTED_MODALITIES, so a flag inherited from a base class instead of assigned on
Plugin passes archive inspection and then fails at load.
Everything outside those two lists is optional and falls back to the BasePlugin default when omitted, including
DEFAULT_CONFIGURATION, BACKEND_VARIANT_OPTIONS, CLONABLE_FIELDS,
WELCOME_MESSAGE, LICENSE_MANAGED_BACKEND, EXTERNAL_PROVIDER_DEFAULTS,
SUPPORTS_GPU_BINDING, SUPPORTS_MODEL_SEARCH,
SUPPORTS_MODEL_VARIANT_DISCOVERY, and SUPPORTS_PROMPT_TOKEN_COUNTING. The last four have a
fallback implementation on the base class that raises a state error when the flag is set and the method is not
overridden, so declaring one of them early turns a missing feature into a failed request rather than a quiet no.
Declare BACKEND_VARIANT_OPTIONS as a literal list, or load a root JSON resource with
plugin_sdk.load_backend_variant_options(__file__, "runtime_variants.json"). Both forms pass the same
static option validation before plugin code executes. See
Keep variant metadata in a static resource
for filename and size limits.
PERSISTENT changes when the plugin runs. A persistent plugin is brought up during initial load, where
SoAI calls get_status and health_ping and attempts start if it is unhealthy,
and the Guardian keeps polling it afterwards. A non-persistent plugin is started on demand around a request through
start_with_model and can be evicted when the instance is at its concurrency limit. Report health
honestly from both methods, since an optimistic health_ping is what turns a broken backend into
repeated dispatch instead of quarantine.
Declare every third-party dependency
Each plugin gets its own Python environment rather than sharing the host runtime, and that environment already
carries the worker baseline of dnspython, httpx2, psutil, pydantic, typing-extensions, and zstandard at the versions
SoAI pins, so those need no declaration. Anything else your code imports needs a pinned entry in
PACKAGE_DEPENDENCIES, and an import with no matching entry fails the audit as a security error rather
than surfacing later as a missing module. The check matches import roots against distribution names, so a
distribution whose import name differs from its package name resolves only when that distribution is installed on
the host and publishes a top_level.txt, with built-in aliases for PyYAML and piper-tts alone. Prefer a
distribution whose import name matches its name.
Implement the BasePlugin contract
Capability declarations do not remove abstract methods. Implement every BasePlugin method, including operations your plugin does not expose through a capability.
| Area | Abstract methods |
|---|---|
| Configuration and backend | get_parameter_schema, get_default_configuration, check_system_dependencies, install_backend, update_backend, and remove_backend. |
| Status and lifecycle | get_status, check_for_backend_update, health_ping, get_log_file_path, discover_models, start, start_with_model, and stop. |
| Models and requests | cancel_task, load_model_in_service, unload_model_in_service, get_model_download_plan, download_model, delete_model, and handle_request. |
The base class supplies defaults for partial-install cleanup and backend-process PID reporting.
Plugin SDK reference lists the contexts, helpers, and error types available
through the top-level SDK, and backend/plugin_sdk/__init__.pyi is the maintained typing stub that gives
an editor the whole lazily resolved surface. Import from plugin_sdk itself and use package-local
relative imports; a plugin_sdk.* submodule import and a direct import of a Core package are both
rejected, as are __import__, eval, exec, compile,
importlib, and runpy in aliased and getattr forms, so an optional dependency
cannot be lazily loaded. That audit always runs on upload whatever
PLUGINS.SECURITY.SAFETY_VALIDATION
is set to, while a package you place in the plugins folder is checked through safety validation instead, which that
setting can disable.
Declare what the host must provide
REQUIRED_SOAI_VERSION is a Python version specifier set matched against the running Core version, so
an open lower bound and a lower bound paired with an exclusive upper bound are both valid, while an unparsable
value marks the package broken. A package outside the range loads as version incompatible with a message naming the update or
downgrade target, and that verdict cannot be overridden. REQUIRED_SYSTEM_CAPABILITIES is a mapping
whose keys are lowercased before comparison, where os, platforms, arch, and
gpu are recognized, values normalize to SoAI's platform identifiers, and any matches
everything; a host that fails one of those is recorded with its own reason and an administrator can override it on
the plugin record. An upload that fails either check is kept rather than deleted, so the card carries the reason and
you delete the package from Plugins before uploading a corrected build.
Install and test the package
-
Sign in as an Administrator with Plugin administration (
PLUGIN_ADMIN). - In the SoAI WebUI, open Plugins, then select Add plugin.
-
Select Upload plugin, drop the
.soaipluginarchive on Drop a plugin here or select that area to choose it, then select Upload plugin. - Watch the modal progress, or select the bell at the right of the header and expand Tasks. Once the task reaches a terminal result, open the new card and read its status and compatibility details.
- Use only the actions the package declares. Install backend, for example, appears only when the current plugin state and capability permit it.
Upload requires
PLUGINS.FEATURES.UPLOADS to be
enabled, and the server refuses a name that already exists in the catalog or on disk, so an upload cannot replace an
earlier test build. For a tight iteration loop, use Add plugin manually instead: copy the archive
into the plugins folder that dialog shows, then restart. Reconciliation re-audits every package in that folder at
each start and the prepared package cache is keyed by the archive hash, so new bytes under the same name are
inspected again rather than reused.
Create a plugin with an AI coding agent
Give the agent access to the SoAI source tree, the backend's official documentation, and a disposable SoAI instance. The current SDK and bundled plugins define the contract. Keep production credentials out of the brief.
Fill in the bracketed fields in the prompt below, leaving a field blank when it does not apply. The Copy button copies the whole prompt.
You are building and verifying a third-party SoAI plugin against the public Plugin SDK.
PLUGIN BRIEF
Plugin filename without extension: [LOWERCASE ID, FOR EXAMPLE acme_chat]
Display name: [DISPLAY NAME]
Plugin purpose: [ONE CONCRETE PARAGRAPH]
Backend or upstream API: [NAME, VERSION, AND OFFICIAL PROJECT OR DOCUMENTATION ROOT]
Local process or remote service: [LOCAL / REMOTE / HYBRID]
Authentication: [NONE / API KEY / OAUTH / OTHER]
Models and discovery source: [HOW MODELS ARE FOUND]
OpenAI endpoints to support: [CHAT COMPLETIONS / RESPONSES / EMBEDDINGS / IMAGES / AUDIO]
OpenAI features to support: [STREAMING / TOOLS / VISION / JSON SCHEMA / OTHER]
Supported operating systems: [LINUX / WINDOWS / MACOS]
Supported hardware: [CPU / NVIDIA / AMD / INTEL / APPLE SILICON]
Backend installation or model downloads: [DESCRIBE OR NONE]
Configuration fields: [NAMES, TYPES, DEFAULTS, AND WHICH HOLD CREDENTIALS]
Cancellation behavior: [HOW AN ACTIVE BACKEND REQUEST IS STOPPED]
License for the plugin: [SPDX NAME OR LICENSE TEXT]
Acceptance test: [THE REQUEST AND RESULT THAT PROVE THE PLUGIN WORKS]
Create the plugin source, focused tests, a deterministic build step, and the final .soaiplugin package. Test the final package in a disposable SoAI installation before treating it as complete.
Public contract
1. Read Creating a new plugin, Plugin SDK reference, Plugin architecture, Install and manage plugins, and Security hardening in the documentation for the SoAI version that will load the package. Treat those published pages and the installed top-level plugin_sdk surface as authoritative. If a requested feature is not expressible through that public surface, stop and identify the missing contract instead of importing private application modules.
2. Determine the current BasePlugin methods, metadata fields, OpenAI capability flags, supported helper types, import rules, and package limits from the public SDK documentation. Implement the exact contract for the target SoAI version. Do not change SoAI or weaken its validation to make the plugin load.
3. Ask for a missing plugin-specific fact only when it cannot be determined from the brief, the upstream project's public documentation, or the public SoAI plugin documentation. Never request or embed a production credential. Use named configuration fields for secrets and ensure their values cannot reach logs, exceptions, fixtures, or the archive.
Package contract
4. Build a ZIP-format file named [PLUGIN ID].soaiplugin. Put exactly one regular __init__.py at the archive root. That entrypoint defines one direct Plugin class derived from plugin_sdk.BasePlugin. Do not wrap the package in another directory.
5. Put cohesive implementation modules and resources beside the entrypoint and use relative imports within the package. Keep tests, fixtures, mocks, build scripts, caches, bytecode, local environments, credentials, and generated .soaiplugin.yaml files outside the archive. Do not bundle plugin_sdk or other SoAI files.
6. Import only the Python standard library, top-level plugin_sdk exports, package-local modules, and third-party distributions declared in PACKAGE_DEPENDENCIES. Do not import plugin_sdk submodules or private SoAI packages. Use static imports and declare only dependencies the plugin actually uses.
7. Set every required metadata and OpenAI capability field directly on Plugin with literal values, including false and empty values. Set capabilities from implemented, tested behavior. Use the plugin metadata names documented for the target SoAI version and declare an accurate REQUIRED_SOAI_VERSION range.
8. Put non-secret defaults in DEFAULT_CONFIGURATION and let SoAI create the runtime configuration sidecar. Include model_parameters.json only when the plugin exposes per-model parameters. Every discovered model must have a stable, non-empty source model ID and truthful provider, format, ownership, and capability data.
Behavior and resilience
9. Implement every required BasePlugin method with the exact public signature. Disabled operations return the documented truthful result; they never fabricate success. Validate inputs before mutation and use atomic writes or explicit rollback for plugin-managed state.
10. Return standard OpenAI-compatible payloads for declared endpoints. Streaming responses emit valid OpenAI server-sent event frames and end with data: [DONE]. Preserve documented tool-call and usage fields. Do not add private SoAI envelopes to OpenAI streams.
11. Use the SDK request context, cancellation hook, HTTP client, file access, configuration, runtime services, and public error types. Enforce offline policy before network access when required. Clean up subprocesses, streams, files, locks, and tracked tasks after success, failure, cancellation, and shutdown. Cancellation must stop only the selected request.
12. Classify invalid input, configuration problems, missing resources, upstream failures, process failures, and invalid state with the matching public SDK error type. Log an unexpected failure once at its owning boundary. Never log secrets, authorization headers, credentials, or unsafe upstream payloads.
13. Retry only idempotent operations with bounded attempts, backoff, cancellation, and duplicate prevention. Installation, updates, removal, model downloads, and model deletion must finish in a truthful success, failed, or cancelled state. Preserve unrelated plugins and models when one operation fails.
Build and acceptance
14. Keep source, tests, build output, and runtime scratch data separate. Write focused tests outside the archive for metadata, dependency declarations, configuration, stable model discovery, health success and failure, request translation, non-streaming output, streaming framing, cancellation, timeouts, malformed upstream data, secret redaction, and cleanup. Test installation, updates, removal, and model-file operations when supported.
15. Make the build reproducible: sort archive paths, normalize ZIP timestamps and regular-file modes, reject symbolic links, inspect the final archive listing, and confirm unchanged source produces the same SHA-256 digest. Keep only intended runtime files in the final archive.
16. Upload the final archive through Plugins > Add plugin > Upload plugin in a disposable SoAI installation. Wait for the upload task to finish, then inspect Plugin information for version, compatibility, state, modalities, capabilities, and file hash. Confirm configuration validation and saving without exposing credentials.
17. Confirm each expected model appears in Models with a stable source identity. Run Test model, complete the acceptance request through Chat or the documented API, verify non-streaming output, verify streaming and [DONE] when supported, and cancel an active request. Check the visible task and plugin states and review logs for tracebacks, secret exposure, repeated failure loops, or quarantine events.
18. Exercise one deliberate health or upstream failure and confirm the plugin becomes truthfully degraded, failed, or quarantined without breaking another plugin. If the plugin manages a local backend or model files, also test interruption, restart recovery, repeat of one idempotent operation, and removal with each supported data-retention choice.
Completion requires the final archive, its SHA-256 digest, passing focused tests, successful SoAI package validation and loading, stable model discovery, the acceptance request, cancellation evidence, and a clear list of capabilities or hardware paths that remain unsupported or untested.