Open Markdown text in a new tab

Creating a new plugin

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

  1. Sign in as an Administrator with Plugin administration (PLUGIN_ADMIN).
  2. In the SoAI WebUI, open Plugins, then select Add plugin.
  3. Select Upload plugin, drop the .soaiplugin archive on Drop a plugin here or select that area to choose it, then select Upload plugin.
  4. 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.
  5. 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.

Related documentation