Building plugins
Создание Plugin для провайдеров
Это руководство пошагово показывает, как создать Plugin провайдера, который добавляет провайдера моделей (LLM) в OpenClaw. К концу у вас будет провайдер с каталогом моделей, аутентификацией по ключу API и динамическим разрешением моделей.
Пошаговое руководство
Пакет и манифест
Шаг 1: Пакет и манифест
{"name": "@myorg/openclaw-acme-ai","version": "1.0.0","type": "module","openclaw": { "extensions": ["./index.ts"], "providers": ["acme-ai"], "compat": { "pluginApi": ">=2026.3.24-beta.2", "minGatewayVersion": "2026.3.24-beta.2" }, "build": { "openclawVersion": "2026.3.24-beta.2", "pluginSdkVersion": "2026.3.24-beta.2" }}}{"id": "acme-ai","name": "Acme AI","description": "Acme AI model provider","providers": ["acme-ai"],"modelSupport": { "modelPrefixes": ["acme-"]},"setup": { "providers": [ { "id": "acme-ai", "envVars": ["ACME_AI_API_KEY"] } ]},"providerAuthAliases": { "acme-ai-coding": "acme-ai"},"providerAuthChoices": [ { "provider": "acme-ai", "method": "api-key", "choiceId": "acme-ai-api-key", "choiceLabel": "Acme AI API key", "groupId": "acme-ai", "groupLabel": "Acme AI", "cliFlag": "--acme-ai-api-key", "cliOption": "--acme-ai-api-key <key>", "cliDescription": "Acme AI API key" }],"configSchema": { "type": "object", "additionalProperties": false}}Манифест объявляет setup.providers[].envVars, чтобы OpenClaw мог обнаруживать
учетные данные без загрузки runtime вашего Plugin. Добавьте providerAuthAliases,
когда вариант провайдера должен повторно использовать аутентификацию id другого провайдера. modelSupport
необязателен и позволяет OpenClaw автоматически загрузить Plugin вашего провайдера по сокращенным
id моделей, таким как acme-large, еще до появления runtime-хуков. Если вы публикуете
провайдера в ClawHub, эти поля openclaw.compat и openclaw.build
обязательны в package.json.
Зарегистрируйте провайдера
Минимальному текстовому провайдеру нужны id, label, auth и catalog.
catalog — это runtime/config-хук, принадлежащий провайдеру; он может вызывать живые
API поставщика и возвращает записи models.providers.
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth"; export default definePluginEntry({ id: "acme-ai", name: "Acme AI", description: "Acme AI model provider", register(api) { api.registerProvider({ id: "acme-ai", label: "Acme AI", docsPath: "/providers/acme-ai", envVars: ["ACME_AI_API_KEY"], auth: [ createProviderApiKeyAuthMethod({ providerId: "acme-ai", methodId: "api-key", label: "Acme AI API key", hint: "API key from your Acme AI dashboard", optionKey: "acmeAiApiKey", flagName: "--acme-ai-api-key", envVar: "ACME_AI_API_KEY", promptMessage: "Enter your Acme AI API key", defaultModel: "acme-ai/acme-large", }), ], catalog: { order: "simple", run: async (ctx) => { const apiKey = ctx.resolveProviderApiKey("acme-ai").apiKey; if (!apiKey) return null; return { provider: { baseUrl: "https://api.acme-ai.com/v1", apiKey, api: "openai-completions", models: [ { id: "acme-large", name: "Acme Large", reasoning: true, input: ["text", "image"], cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, contextWindow: 200000, maxTokens: 32768, }, { id: "acme-small", name: "Acme Small", reasoning: false, input: ["text"], cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 }, contextWindow: 128000, maxTokens: 8192, }, ], }, }; }, }, }); api.registerModelCatalogProvider({ provider: "acme-ai", kinds: ["text"], liveCatalog: async (ctx) => { const apiKey = ctx.resolveProviderApiKey("acme-ai").apiKey; if (!apiKey) return null; return [ { kind: "text", provider: "acme-ai", model: "acme-large", label: "Acme Large", source: "live", }, ]; }, }); },});registerModelCatalogProvider — это более новая поверхность каталога control-plane
для UI списков, справки и выбора. Используйте ее для строк text, image-generation,
video-generation и music-generation. Оставляйте вызовы endpoint поставщика и
сопоставление ответов в Plugin; OpenClaw владеет общей формой строк, метками
источников и отрисовкой справки.
Это рабочий провайдер. Теперь пользователи могут выполнить
openclaw onboard --acme-ai-api-key <key> и выбрать
acme-ai/acme-large в качестве модели.
Живое обнаружение моделей
Если ваш провайдер предоставляет API в стиле /models, оставьте специфичный для провайдера
endpoint и проекцию строк в вашем Plugin и используйте
openclaw/plugin-sdk/provider-catalog-live-runtime для общего lifecycle выборки.
Этот helper дает защищенные HTTP-запросы, заголовки аутентификации провайдера,
структурированные HTTP-ошибки, TTL-кэширование и поведение статического fallback без
помещения политики провайдера в ядро OpenClaw.
Используйте buildLiveModelProviderConfig, когда live API сообщает только, какие
принадлежащие провайдеру строки статического каталога сейчас доступны:
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";import { buildLiveModelProviderConfig, type LiveModelCatalogFetchGuard,} from "openclaw/plugin-sdk/provider-catalog-live-runtime"; const STATIC_MODELS = [ { id: "acme-large", name: "Acme Large", reasoning: true, input: ["text", "image"], cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }, contextWindow: 200000, maxTokens: 32768, }, { id: "acme-small", name: "Acme Small", reasoning: false, input: ["text"], cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 }, contextWindow: 128000, maxTokens: 8192, },] as const; async function buildAcmeLiveProvider(params: { apiKey: string; discoveryApiKey?: string; fetchGuard?: LiveModelCatalogFetchGuard;}) { return await buildLiveModelProviderConfig({ providerId: "acme-ai", endpoint: "https://api.acme-ai.com/v1/models", providerConfig: { baseUrl: "https://api.acme-ai.com/v1", api: "openai-completions", }, models: STATIC_MODELS, apiKey: params.apiKey, discoveryApiKey: params.discoveryApiKey, fetchGuard: params.fetchGuard, ttlMs: 60_000, auditContext: "acme-ai-model-discovery", });} export default definePluginEntry({ id: "acme-ai", name: "Acme AI", register(api) { api.registerProvider({ id: "acme-ai", label: "Acme AI", catalog: { order: "simple", run: async (ctx) => { const auth = ctx.resolveProviderAuth("acme-ai"); const apiKey = auth.apiKey ?? ctx.resolveProviderApiKey("acme-ai").apiKey; if (!apiKey) return null; return { provider: await buildAcmeLiveProvider({ apiKey, discoveryApiKey: auth.discoveryApiKey, }), }; }, }, staticCatalog: { order: "simple", run: async () => ({ provider: { baseUrl: "https://api.acme-ai.com/v1", api: "openai-completions", models: [...STATIC_MODELS], }, }), }, }); },});Используйте getCachedLiveProviderModelRows, когда API провайдера возвращает более богатые
метаданные, а Plugin должен сам проецировать строки в определения моделей OpenClaw:
import { getCachedLiveProviderModelRows, LiveModelCatalogHttpError,} from "openclaw/plugin-sdk/provider-catalog-live-runtime"; async function discoverAcmeModels(apiKey: string) { try { const rows = await getCachedLiveProviderModelRows({ providerId: "acme-ai", endpoint: "https://api.acme-ai.com/v1/models", apiKey, ttlMs: 60_000, auditContext: "acme-ai-model-discovery", }); return rows .map((row) => projectAcmeModel(row)) .filter((model) => model !== null); } catch (error) { if (error instanceof LiveModelCatalogHttpError) { return STATIC_MODELS; } throw error; }}run должен оставаться закрытым аутентификацией и возвращать null, когда нет пригодных
учетных данных. Сохраняйте offline staticRun или статический fallback, чтобы setup, docs,
tests и поверхности выбора не зависели от живого доступа к сети. Используйте TTL,
подходящий для свежести списка моделей, избегайте опроса файловой системы во время запроса
и передавайте специфичные для провайдера readRows / readModelId только тогда, когда
ответ upstream не имеет OpenAI-совместимую форму { data: [{ id, object }] }.
Если upstream-провайдер использует управляющие токены, отличные от OpenClaw, добавьте небольшое двунаправленное текстовое преобразование вместо замены пути потока:
api.registerTextTransforms({ input: [ { from: /red basket/g, to: "blue basket" }, { from: /paper ticket/g, to: "digital ticket" }, { from: /left shelf/g, to: "right shelf" }, ], output: [ { from: /blue basket/g, to: "red basket" }, { from: /digital ticket/g, to: "paper ticket" }, { from: /right shelf/g, to: "left shelf" }, ],});input переписывает финальный системный prompt и содержимое текстовых сообщений перед
transport. output переписывает текстовые дельты assistant и финальный текст до того, как
OpenClaw разбирает собственные управляющие маркеры или доставку в канал.
Для встроенных провайдеров, которые регистрируют только одного текстового провайдера с
аутентификацией по ключу API и единственным runtime на основе каталога, предпочитайте более узкий
helper defineSingleProviderPluginEntry(...):
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; export default defineSingleProviderPluginEntry({ id: "acme-ai", name: "Acme AI", description: "Acme AI model provider", provider: { label: "Acme AI", docsPath: "/providers/acme-ai", auth: [ { methodId: "api-key", label: "Acme AI API key", hint: "API key from your Acme AI dashboard", optionKey: "acmeAiApiKey", flagName: "--acme-ai-api-key", envVar: "ACME_AI_API_KEY", promptMessage: "Enter your Acme AI API key", defaultModel: "acme-ai/acme-large", }, ], catalog: { buildProvider: () => ({ api: "openai-completions", baseUrl: "https://api.acme-ai.com/v1", models: [{ id: "acme-large", name: "Acme Large" }], }), buildStaticProvider: () => ({ api: "openai-completions", baseUrl: "https://api.acme-ai.com/v1", models: [{ id: "acme-large", name: "Acme Large" }], }), }, },});buildProvider — это путь живого каталога, используемый, когда OpenClaw может разрешить реальную
аутентификацию поставщика. Он может выполнять обнаружение, специфичное для поставщика. Используйте
buildStaticProvider только для офлайн-строк, которые безопасно показывать до настройки
аутентификации; он не должен требовать учетных данных или выполнять сетевые запросы.
Отображение models list --all в OpenClaw сейчас выполняет статические каталоги
только для встроенных Plugin поставщиков, с пустой конфигурацией, пустым окружением и без
путей агента/рабочей области.
Если вашему потоку аутентификации также нужно изменять models.providers.*, псевдонимы и
модель агента по умолчанию во время onboarding, используйте готовые вспомогательные функции из
openclaw/plugin-sdk/provider-onboard. Самые узкие вспомогательные функции:
createDefaultModelPresetAppliers(...),
createDefaultModelsPresetAppliers(...) и
createModelCatalogPresetAppliers(...).
Когда нативная конечная точка поставщика поддерживает потоковые блоки usage поверх
обычного транспорта openai-completions, предпочитайте общие вспомогательные функции каталога в
openclaw/plugin-sdk/provider-catalog-shared вместо жестко заданных
проверок id поставщика. supportsNativeStreamingUsageCompat(...) и
applyProviderNativeStreamingUsageCompat(...) определяют поддержку по карте
возможностей конечной точки, поэтому нативные конечные точки в стиле Moonshot/DashScope все равно
включаются, даже если Plugin использует пользовательский id поставщика.
Примеры живого обнаружения выше покрывают API поставщиков в стиле /models. Держите
это обнаружение внутри catalog.run, с ограничением по пригодной аутентификации, и держите
staticRun без сетевых запросов для генерации офлайн-каталога.
Add dynamic model resolution
Если ваш поставщик принимает произвольные ID моделей (например, прокси или маршрутизатор),
добавьте resolveDynamicModel:
api.registerProvider({ // ... id, label, auth, catalog from above resolveDynamicModel: (ctx) => ({ id: ctx.modelId, name: ctx.modelId, provider: "acme-ai", api: "openai-completions", baseUrl: "https://api.acme-ai.com/v1", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 128000, maxTokens: 8192, }),});Если разрешение требует сетевого вызова, используйте prepareDynamicModel для асинхронного
прогрева - resolveDynamicModel запустится снова после его завершения.
Add runtime hooks (as needed)
Большинству поставщиков нужны только catalog + resolveDynamicModel. Добавляйте hooks
постепенно, по мере необходимости для вашего поставщика.
Общие сборщики вспомогательных функций теперь покрывают самые распространенные семейства replay/tool-compat, поэтому Plugin обычно не нужно вручную подключать каждый hook по одному:
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";import { buildProviderStreamFamilyHooks } from "openclaw/plugin-sdk/provider-stream";import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools"; const GOOGLE_FAMILY_HOOKS = { ...buildProviderReplayFamilyHooks({ family: "google-gemini" }), ...buildProviderStreamFamilyHooks("google-thinking"), ...buildProviderToolCompatFamilyHooks("gemini"),}; api.registerProvider({ id: "acme-gemini-compatible", // ... ...GOOGLE_FAMILY_HOOKS,});Доступные сегодня семейства replay:
| Семейство | Что подключается | Встроенные примеры |
|---|---|---|
openai-compatible |
Общая политика replay в стиле OpenAI для OpenAI-совместимых транспортов, включая очистку tool-call-id, исправления порядка с первым сообщением assistant и общую валидацию ходов Gemini там, где это нужно транспорту | moonshot, ollama, xai, zai |
anthropic-by-model |
Политика replay с учетом Claude, выбираемая по modelId, чтобы транспорты Anthropic-message получали очистку thinking-block, специфичную для Claude, только когда разрешенная модель действительно имеет id Claude |
amazon-bedrock, anthropic-vertex |
google-gemini |
Нативная политика replay Gemini плюс очистка bootstrap replay. Общее семейство сохраняет текстовый вывод Gemini CLI на tagged reasoning; прямой поставщик google переопределяет resolveReasoningOutputMode на native, потому что thinking в Gemini API приходит как нативные thought parts. |
google, google-gemini-cli |
passthrough-gemini |
Очистка thought-signature Gemini для моделей Gemini, работающих через OpenAI-совместимые прокси-транспорты; не включает нативную валидацию replay Gemini или bootstrap-перезаписи | openrouter, kilocode, opencode, opencode-go |
hybrid-anthropic-openai |
Гибридная политика для поставщиков, которые смешивают поверхности моделей Anthropic-message и OpenAI-совместимые поверхности моделей в одном Plugin; опциональное удаление thinking-block только для Claude остается ограниченным стороной Anthropic | minimax |
Доступные сегодня семейства stream:
| Семейство | Что подключается | Встроенные примеры |
|---|---|---|
google-thinking |
Нормализация thinking payload Gemini на общем stream-пути | google, google-gemini-cli |
kilocode-thinking |
Обертка Kilo reasoning на общем stream-пути прокси, при этом kilo/auto и неподдерживаемые id reasoning прокси пропускают внедренный thinking |
kilocode |
moonshot-thinking |
Маппинг бинарного payload нативного thinking Moonshot из конфигурации + уровня /think |
moonshot |
minimax-fast-mode |
Перезапись модели MiniMax fast-mode на общем stream-пути | minimax, minimax-portal |
openai-responses-defaults |
Общие обертки нативных OpenAI/Codex Responses: заголовки атрибуции, /fast/serviceTier, подробность текста, нативный веб-поиск Codex, формирование reasoning-compat payload и управление контекстом Responses |
openai |
openrouter-thinking |
Обертка reasoning OpenRouter для прокси-маршрутов, с централизованной обработкой пропусков unsupported-model/auto |
openrouter |
tool-stream-default-on |
Включенная по умолчанию обертка tool_stream для поставщиков вроде Z.AI, которым нужен tool streaming, если он явно не отключен |
zai |
SDK seams powering the family builders
Каждый сборщик семейства составлен из низкоуровневых публичных вспомогательных функций, экспортируемых из того же пакета, к которым можно обратиться, когда поставщику нужно отойти от общего шаблона:
openclaw/plugin-sdk/provider-model-shared-ProviderReplayFamily,buildProviderReplayFamilyHooks(...)и необработанные сборщики replay (buildOpenAICompatibleReplayPolicy,buildAnthropicReplayPolicyForModel,buildGoogleGeminiReplayPolicy,buildHybridAnthropicOrOpenAIReplayPolicy). Также экспортирует вспомогательные функции replay Gemini (sanitizeGoogleGeminiReplayHistory,resolveTaggedReasoningOutputMode) и вспомогательные функции конечных точек/моделей (resolveProviderEndpoint,normalizeProviderId,normalizeGooglePreviewModelId).openclaw/plugin-sdk/provider-stream-ProviderStreamFamily,buildProviderStreamFamilyHooks(...),composeProviderStreamWrappers(...), а также общие обертки OpenAI/Codex (createOpenAIAttributionHeadersWrapper,createOpenAIFastModeWrapper,createOpenAIServiceTierWrapper,createOpenAIResponsesContextManagementWrapper,createCodexNativeWebSearchWrapper), OpenAI-совместимая обертка DeepSeek V4 (createDeepSeekV4OpenAICompatibleThinkingWrapper), очистка thinking prefill для Anthropic Messages (createAnthropicThinkingPrefillPayloadWrapper), совместимость plain-text tool-call (createPlainTextToolCallCompatWrapper) и общие обертки прокси/поставщиков (createOpenRouterWrapper,createToolStreamWrapper,createMinimaxFastModeWrapper).openclaw/plugin-sdk/provider-stream-shared- легковесные обертки payload и событий для горячих путей поставщиков, включаяcreateOpenAICompatibleCompletionsThinkingOffWrapper,createPayloadPatchStreamWrapper,createPlainTextToolCallCompatWrapper,normalizeOpenAICompatibleReasoningPayload(...)иsetQwenChatTemplateThinking(...).openclaw/plugin-sdk/provider-tools-ProviderToolCompatFamily,buildProviderToolCompatFamilyHooks("deepseek" | "gemini" | "openai")и базовые вспомогательные функции схем поставщиков.
Для поставщиков семейства Gemini держите режим reasoning-output согласованным с
транспортом. Прямые поставщики Google Gemini API должны использовать native
reasoning output, чтобы OpenClaw потреблял нативные thought parts без добавления
prompt-директив <think> / <final>. Текстовые backends в стиле Gemini CLI,
которые разбирают финальный JSON/текстовый ответ, могут сохранять общий
tagged-контракт google-gemini.
Некоторые stream-вспомогательные функции намеренно остаются локальными для поставщика. @openclaw/anthropic-provider держит wrapAnthropicProviderStream, resolveAnthropicBetas, resolveAnthropicFastMode, resolveAnthropicServiceTier и низкоуровневые сборщики оберток Anthropic в собственном публичном шве api.ts / contract-api.ts, потому что они кодируют обработку Claude OAuth beta и gating context1m. Plugin xAI аналогично держит формирование нативных xAI Responses в собственном wrapStreamFn (псевдонимы /fast, tool_stream по умолчанию, очистка неподдерживаемого strict-tool, удаление reasoning-payload, специфичное для xAI).
Тот же шаблон package-root также поддерживает @openclaw/openai-provider (сборщики поставщика, вспомогательные функции модели по умолчанию, сборщики realtime-поставщика) и @openclaw/openrouter-provider (сборщик поставщика плюс вспомогательные функции onboarding/config).
Token exchange
Для поставщиков, которым нужен обмен токена перед каждым вызовом inference:
prepareRuntimeAuth: async (ctx) => { const exchanged = await exchangeToken(ctx.apiKey); return { apiKey: exchanged.token, baseUrl: exchanged.baseUrl, expiresAt: exchanged.expiresAt, };},Custom headers
Для поставщиков, которым нужны пользовательские заголовки запросов или изменения тела:
// wrapStreamFn returns a StreamFn derived from ctx.streamFnwrapStreamFn: (ctx) => { if (!ctx.streamFn) return undefined; const inner = ctx.streamFn; return async (params) => { params.headers = { ...params.headers, "X-Acme-Version": "2", }; return inner(params); };},Native transport identity
Для поставщиков, которым нужны нативные заголовки запросов/сессий или метаданные на универсальных HTTP- или WebSocket-транспортах:
resolveTransportTurnState: (ctx) => ({ headers: { "x-request-id": ctx.turnId, }, metadata: { session_id: ctx.sessionId ?? "", turn_id: ctx.turnId, },}),resolveWebSocketSessionPolicy: (ctx) => ({ headers: { "x-session-id": ctx.sessionId ?? "", }, degradeCooldownMs: 60_000,}),Usage and billing
Для провайдеров, которые предоставляют данные об использовании/оплате:
resolveUsageAuth: async (ctx) => { const auth = await ctx.resolveOAuthToken(); return auth ? { token: auth.token } : null;},fetchUsageSnapshot: async (ctx) => { return await fetchAcmeUsage(ctx.token, ctx.timeoutMs);},resolveUsageAuth имеет три исхода. Возвращайте { token, accountId? },
когда у провайдера есть учетные данные для использования/оплаты. Возвращайте
{ handled: true } только когда провайдер окончательно обработал авторизацию
использования, но у него нет пригодного токена использования, и OpenClaw должен
пропустить общий резервный путь API-ключа/OAuth. Возвращайте null или
undefined, когда провайдер не обработал запрос и OpenClaw должен продолжить
общий резервный путь.
All available provider hooks
OpenClaw вызывает хуки в таком порядке. Большинство провайдеров используют только 2-3:
Поля провайдера только для совместимости, которые OpenClaw больше не вызывает, например
ProviderPlugin.capabilities и suppressBuiltInModel, здесь не перечислены.
| # | Хук | Когда использовать |
|---|---|---|
| 1 | catalog |
Каталог моделей или значения по умолчанию для базового URL |
| 2 | applyConfigDefaults |
Глобальные значения по умолчанию, принадлежащие провайдеру, при материализации конфигурации |
| 3 | normalizeModelId |
Очистка устаревших/предварительных псевдонимов ID модели перед поиском |
| 4 | normalizeTransport |
Очистка api / baseUrl для семейства провайдера перед общей сборкой модели |
| 5 | normalizeConfig |
Нормализация конфигурации models.providers.<id> |
| 6 | applyNativeStreamingUsageCompat |
Перезаписи совместимости нативного потокового учета использования для конфигурационных провайдеров |
| 7 | resolveConfigApiKey |
Разрешение авторизации по env-маркерам, принадлежащее провайдеру |
| 8 | resolveSyntheticAuth |
Локальная/самостоятельно размещенная или основанная на конфигурации синтетическая авторизация |
| 9 | shouldDeferSyntheticProfileAuth |
Понижение приоритета синтетических заполнителей сохраненного профиля за env/config-авторизацией |
| 10 | resolveDynamicModel |
Прием произвольных ID моделей upstream |
| 11 | prepareDynamicModel |
Асинхронная загрузка метаданных перед разрешением |
| 12 | normalizeResolvedModel |
Перезаписи транспорта перед runner |
| 13 | normalizeToolSchemas |
Очистка схем инструментов, принадлежащая провайдеру, перед регистрацией |
| 14 | inspectToolSchemas |
Диагностика схем инструментов, принадлежащая провайдеру |
| 15 | resolveReasoningOutputMode |
Контракт tagged vs native reasoning-output |
| 16 | prepareExtraParams |
Параметры запроса по умолчанию |
| 17 | createStreamFn |
Полностью пользовательский транспорт StreamFn |
| 19 | wrapStreamFn |
Пользовательские обертки headers/body на обычном пути потока |
| 20 | resolveTransportTurnState |
Нативные headers/metadata для каждого turn |
| 21 | resolveWebSocketSessionPolicy |
Нативные headers/cool-down для WS-сеанса |
| 22 | formatApiKey |
Пользовательская форма runtime-токена |
| 23 | refreshOAuth |
Пользовательское обновление OAuth |
| 24 | buildAuthDoctorHint |
Подсказка по исправлению авторизации |
| 25 | matchesContextOverflowError |
Обнаружение переполнения, принадлежащее провайдеру |
| 26 | classifyFailoverReason |
Классификация rate-limit/overload, принадлежащая провайдеру |
| 27 | isCacheTtlEligible |
Ограничение TTL кеша промптов |
| 28 | buildMissingAuthMessage |
Пользовательская подсказка об отсутствующей авторизации |
| 29 | augmentModelCatalog |
Синтетические строки forward-compat |
| 30 | resolveThinkingProfile |
Набор параметров /think, специфичный для модели |
| 31 | isBinaryThinking |
Совместимость бинарного включения/выключения thinking |
| 32 | supportsXHighThinking |
Совместимость поддержки reasoning xhigh |
| 33 | resolveDefaultThinkingLevel |
Совместимость политики /think по умолчанию |
| 34 | isModernModelRef |
Сопоставление моделей live/smoke |
| 35 | prepareRuntimeAuth |
Обмен токена перед inference |
| 36 | resolveUsageAuth |
Пользовательский разбор учетных данных использования |
| 37 | fetchUsageSnapshot |
Пользовательский endpoint использования |
| 38 | createEmbeddingProvider |
Адаптер embedding, принадлежащий провайдеру, для memory/search |
| 39 | buildReplayPolicy |
Пользовательская политика replay/compaction транскрипта |
| 40 | sanitizeReplayHistory |
Специфичные для провайдера перезаписи replay после общей очистки |
| 41 | validateReplayTurns |
Строгая валидация replay-turn перед встроенным runner |
| 42 | onModelSelected |
Callback после выбора (например, telemetry) |
Примечания о резервном поведении runtime:
normalizeConfigсначала проверяет совпавшего провайдера, затем другие provider plugins с поддержкой хуков, пока один из них действительно не изменит конфигурацию. Если ни один хук провайдера не переписывает поддерживаемую запись конфигурации семейства Google, встроенный нормализатор конфигурации Google все равно применяется.resolveConfigApiKeyиспользует хук провайдера, когда он предоставлен. Amazon Bedrock держит разрешение AWS env-маркеров в своем provider plugin; сама runtime-авторизация все равно использует цепочку AWS SDK по умолчанию, когда настроена сauth: "aws-sdk".resolveThinkingProfile(ctx)получает выбранныеprovider,modelId, необязательную объединенную подсказку каталогаreasoningи необязательные объединенные фактыcompatмодели. Используйтеcompatтолько для выбора thinking UI/profile провайдера.resolveSystemPromptContributionпозволяет провайдеру внедрять cache-aware guidance для system-prompt для семейства моделей. Предпочитайте его вместоbefore_prompt_build, когда поведение принадлежит одному провайдеру/семейству моделей и должно сохранять разделение stable/dynamic cache.
Подробные описания и реальные примеры см. в Внутреннее устройство: хуки runtime провайдера.
Add extra capabilities (optional)
Шаг 5: Добавьте дополнительные возможности
Provider plugin может регистрировать embeddings, speech, realtime transcription, realtime voice, media understanding, image generation, video generation, web fetch и web search вместе с text inference. OpenClaw классифицирует это как плагин с гибридными возможностями - рекомендуемый шаблон для плагинов компаний (один плагин на поставщика). См. Внутреннее устройство: владение возможностями.
Регистрируйте каждую возможность внутри register(api) рядом с существующим
вызовом api.registerProvider(...). Выберите только нужные вкладки:
Speech (TTS)
import { assertOkOrThrowProviderError, postJsonRequest,} from "openclaw/plugin-sdk/provider-http"; api.registerSpeechProvider({ id: "acme-ai", label: "Acme Speech", defaultTimeoutMs: 120_000, isConfigured: ({ config }) => Boolean(config.messages?.tts), synthesize: async (req) => { const { response, release } = await postJsonRequest({ url: "https://api.example.com/v1/speech", headers: new Headers({ "Content-Type": "application/json" }), body: { text: req.text }, timeoutMs: req.timeoutMs, fetchFn: fetch, auditContext: "acme speech", }); try { await assertOkOrThrowProviderError(response, "Acme Speech API error"); return { audioBuffer: Buffer.from(await response.arrayBuffer()), outputFormat: "mp3", fileExtension: ".mp3", voiceCompatible: false, }; } finally { await release(); } },});Используйте assertOkOrThrowProviderError(...) для HTTP-сбоев провайдера, чтобы
плагины совместно использовали ограниченное чтение тела ошибки, разбор JSON-ошибок и
suffixes request-id.
Realtime transcription
Предпочитайте createRealtimeTranscriptionWebSocketSession(...) - общий
helper обрабатывает proxy capture, reconnect backoff, close flushing, ready
handshakes, audio queueing и диагностику close-event. Ваш плагин
только сопоставляет upstream-события.
api.registerRealtimeTranscriptionProvider({ id: "acme-ai", label: "Acme Realtime Transcription", isConfigured: () => true, createSession: (req) => { const apiKey = String(req.providerConfig.apiKey ?? ""); return createRealtimeTranscriptionWebSocketSession({ providerId: "acme-ai", callbacks: req, url: "wss://api.example.com/v1/realtime-transcription", headers: { Authorization: `Bearer ${apiKey}` }, onMessage: (event, transport) => { if (event.type === "session.created") { transport.sendJson({ type: "session.update" }); transport.markReady(); return; } if (event.type === "transcript.final") { req.onTranscript?.(event.text); } }, sendAudio: (audio, transport) => { transport.sendJson({ type: "audio.append", audio: audio.toString("base64"), }); }, onClose: (transport) => { transport.sendJson({ type: "audio.end" }); }, }); },});Batch STT-провайдеры, которые отправляют multipart-аудио через POST, должны использовать
buildAudioTranscriptionFormData(...) из
openclaw/plugin-sdk/provider-http. Helper нормализует имена файлов для загрузки,
включая AAC-загрузки, которым нужно имя файла в стиле M4A для
совместимых API транскрипции.
Realtime voice
api.registerRealtimeVoiceProvider({ id: "acme-ai", label: "Acme Realtime Voice", capabilities: { transports: ["gateway-relay"], inputAudioFormats: [{ encoding: "pcm16", sampleRateHz: 24000, channels: 1 }], outputAudioFormats: [{ encoding: "pcm16", sampleRateHz: 24000, channels: 1 }], supportsBargeIn: true, supportsToolCalls: true, }, isConfigured: ({ providerConfig }) => Boolean(providerConfig.apiKey), createBridge: (req) => ({ // Set this only if the provider accepts multiple tool responses for // one call, for example an immediate "working" response followed by // the final result. supportsToolResultContinuation: false, connect: async () => {}, sendAudio: () => {}, setMediaTimestamp: () => {}, handleBargeIn: () => {}, submitToolResult: () => {}, acknowledgeMark: () => {}, close: () => {}, isConnected: () => true, }),});Объявите capabilities, чтобы talk.catalog мог предоставлять допустимые режимы,
транспорты, аудиоформаты и флаги функций браузерным и нативным клиентам
Talk. Реализуйте handleBargeIn, когда транспорт может определить, что
человек прерывает воспроизведение ассистента, а провайдер поддерживает
усечение или очистку активного аудиоответа.
Понимание медиа
api.registerMediaUnderstandingProvider({ id: "acme-ai", capabilities: ["image", "audio"], describeImage: async (req) => ({ text: "A photo of..." }), transcribeAudio: async (req) => ({ text: "Transcript..." }),});Локальные или самостоятельно размещенные медиапровайдеры, которым намеренно
не требуются учетные данные, могут предоставлять resolveAuth и возвращать
kind: "none". OpenClaw по-прежнему сохраняет обычную проверку
аутентификации для провайдеров, которые явно не включили этот режим.
Существующие провайдеры могут продолжать читать req.apiKey; новым
провайдерам следует предпочитать req.auth.
api.registerMediaUnderstandingProvider({ id: "local-audio", capabilities: ["audio"], resolveAuth: () => ({ kind: "none", source: "local-audio plugin no-auth", }), transcribeAudio: async (req) => ({ text: "Transcript..." }),});Embeddings
api.registerEmbeddingProvider({ id: "acme-ai", defaultModel: "acme-embed", transport: "remote", authProviderId: "acme-ai", create: async ({ model }) => ({ provider: { id: "acme-ai", model, dimensions: 1536, embed: async (input) => { const text = typeof input === "string" ? input : input.text; return fetchAcmeEmbedding(text); }, embedBatch: async (inputs) => Promise.all( inputs.map((input) => fetchAcmeEmbedding(typeof input === "string" ? input : input.text), ), ), }, }),});Объявите тот же id в contracts.embeddingProviders. Это общий контракт
embeddings для повторно используемой генерации векторов, включая поиск по
памяти. registerMemoryEmbeddingProvider(...) устарел и оставлен как
совместимость для существующих адаптеров, специфичных для памяти.
Генерация изображений и видео
Возможности видео используют форму, учитывающую режим: generate,
imageToVideo и videoToVideo. Плоских агрегированных полей вроде
maxInputImages / maxInputVideos / maxDurationSeconds недостаточно,
чтобы корректно объявлять поддержку режимов преобразования или отключенные
режимы. Генерация музыки следует тому же шаблону с явными блоками
generate / edit.
api.registerImageGenerationProvider({ id: "acme-ai", label: "Acme Images", generate: async (req) => ({ /* image result */ }),}); api.registerVideoGenerationProvider({ id: "acme-ai", label: "Acme Video", defaultTimeoutMs: 600_000, capabilities: { generate: { maxVideos: 1, maxDurationSeconds: 10, supportsResolution: true }, imageToVideo: { enabled: true, maxVideos: 1, maxInputImages: 1, maxInputImagesByModel: { "acme/reference-to-video": 9 }, maxDurationSeconds: 5, }, videoToVideo: { enabled: false }, }, generateVideo: async (req) => ({ videos: [] }),});Веб-загрузка и поиск
api.registerWebFetchProvider({ id: "acme-ai-fetch", label: "Acme Fetch", hint: "Fetch pages through Acme's rendering backend.", envVars: ["ACME_FETCH_API_KEY"], placeholder: "acme-...", signupUrl: "https://acme.example.com/fetch", credentialPath: "plugins.entries.acme.config.webFetch.apiKey", getCredentialValue: (fetchConfig) => fetchConfig?.acme?.apiKey, setCredentialValue: (fetchConfigTarget, value) => { const acme = (fetchConfigTarget.acme ??= {}); acme.apiKey = value; }, createTool: () => ({ description: "Fetch a page through Acme Fetch.", parameters: {}, execute: async (args) => ({ content: [] }), }),}); api.registerWebSearchProvider({ id: "acme-ai-search", label: "Acme Search", search: async (req) => ({ content: [] }),});Тестирование
Шаг 6: Тестирование
import { describe, it, expect } from "vitest";// Export your provider config object from index.ts or a dedicated fileimport { acmeProvider } from "./provider.js"; describe("acme-ai provider", () => { it("resolves dynamic models", () => { const model = acmeProvider.resolveDynamicModel!({ modelId: "acme-beta-v3", } as any); expect(model.id).toBe("acme-beta-v3"); expect(model.provider).toBe("acme-ai"); }); it("returns catalog when key is available", async () => { const result = await acmeProvider.catalog!.run({ resolveProviderApiKey: () => ({ apiKey: "test-key" }), } as any); expect(result?.provider?.models).toHaveLength(2); }); it("returns null catalog when no key", async () => { const result = await acmeProvider.catalog!.run({ resolveProviderApiKey: () => ({ apiKey: undefined }), } as any); expect(result).toBeNull(); });});Публикация в ClawHub
Plugin провайдеров публикуются так же, как и любой другой внешний кодовый Plugin:
clawhub package publish your-org/your-plugin --dry-runclawhub package publish your-org/your-pluginНе используйте здесь устаревший псевдоним публикации только для Skills; пакеты
Plugin должны использовать clawhub package publish.
Структура файлов
<bundled-plugin-root>/acme-ai/├── package.json # openclaw.providers metadata├── openclaw.plugin.json # Manifest with provider auth metadata├── index.ts # definePluginEntry + registerProvider└── src/ ├── provider.test.ts # Tests └── usage.ts # Usage endpoint (optional)Справочник порядка каталога
catalog.order управляет тем, когда ваш каталог объединяется относительно
встроенных провайдеров:
| Порядок | Когда | Сценарий использования |
|---|---|---|
simple |
Первый проход | Обычные провайдеры с API-ключом |
profile |
После simple | Провайдеры, зависящие от профилей аутентификации |
paired |
После profile | Синтез нескольких связанных записей |
late |
Последний проход | Переопределение существующих провайдеров (побеждает при конфликте) |
Следующие шаги
- Plugin каналов - если ваш Plugin также предоставляет канал
- Среда выполнения SDK - вспомогательные средства
api.runtime(TTS, поиск, subagent) - Обзор SDK - полный справочник импортов по подпутям
- Внутреннее устройство Plugin - подробности хуков и встроенные примеры