Building plugins
Создание плагинов провайдеров
Создайте плагин провайдера, чтобы добавить провайдера моделей (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 обнаруживать учетные данные без
загрузки среды выполнения вашего плагина. Добавьте providerAuthAliases, когда вариант
провайдера должен повторно использовать аутентификацию идентификатора другого провайдера. modelSupport
необязателен и позволяет OpenClaw автоматически загружать ваш плагин провайдера по сокращенным
идентификаторам моделей, таким как acme-large, до появления обработчиков среды выполнения. openclaw.compat
и openclaw.build в package.json обязательны для публикации в ClawHub
(openclaw.compat.pluginApi и openclaw.build.openclawVersion —
два обязательных поля; при отсутствии minGatewayVersion используется
openclaw.install.minHostVersion).
Регистрация провайдера
Минимальному текстовому провайдеру требуются id, label, auth и catalog.
catalog — принадлежащий провайдеру обработчик среды выполнения и конфигурации; он может вызывать действующие
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 — новый интерфейс каталога плоскости управления
для пользовательского интерфейса списков, справки и выбора, охватывающий строки text, voice, image_generation,
video_generation и music_generation. Оставляйте вызовы конечных точек
поставщика и преобразование ответов в плагине; OpenClaw отвечает за общую форму
строк, метки источников и отображение справки.
Теперь провайдер готов к работе. Пользователи могут выполнить
openclaw onboard --acme-ai-api-key <key> и выбрать
acme-ai/acme-large в качестве модели.
Динамическое обнаружение моделей
Если ваш провайдер предоставляет API в стиле /models, оставьте специфичные для провайдера
конечную точку и преобразование строк в своем плагине и используйте
openclaw/plugin-sdk/provider-catalog-live-runtime для общего жизненного
цикла получения данных. Вспомогательная функция предоставляет защищенные HTTP-запросы, заголовки аутентификации провайдера,
структурированные HTTP-ошибки, кэширование с TTL и статическое резервное поведение,
не помещая политику провайдера в ядро OpenClaw.
Используйте buildLiveModelProviderConfig, когда динамический 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 провайдера возвращает более подробные
метаданные и плагину необходимо самостоятельно преобразовывать строки в определения
моделей 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, если
подходящие учетные данные недоступны. Предоставьте автономный staticRun или статический резервный вариант, чтобы настройка, документация,
тесты и интерфейсы выбора не зависели от доступа к сети. Используйте TTL,
соответствующий требованиям к актуальности списка моделей, избегайте опроса файловой системы во время обработки запросов
и передавайте специфичные для провайдера readRows / readModelId только тогда, когда
ответ вышестоящего сервиса не соответствует совместимой с OpenAI структуре { data: [{ id, object }] }.
Если вышестоящий провайдер использует управляющие токены, отличные от 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 преобразует итоговую системную инструкцию и текстовое содержимое сообщений перед
передачей. output преобразует фрагменты текста ассистента и итоговый текст до того, как
OpenClaw обработает собственные управляющие маркеры или доставку в канал.
Для встроенных провайдеров, которые регистрируют только один текстовый провайдер с аутентификацией
по API-ключу и единственной средой выполнения на основе каталога, предпочтительнее использовать более узкую
вспомогательную функцию 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 обрабатывает статические каталоги
только для встроенных плагинов провайдеров, с пустой конфигурацией, пустым окружением и без
путей агента или рабочего пространства.
Если вашему процессу аутентификации также требуется изменять models.providers.*, псевдонимы и
модель агента по умолчанию во время первоначальной настройки, используйте вспомогательные функции предустановок из
openclaw/plugin-sdk/provider-onboard. Наиболее узкие вспомогательные функции:
createDefaultModelPresetAppliers(...),
createDefaultModelsPresetAppliers(...) и
createModelCatalogPresetAppliers(...).
Если нативная конечная точка провайдера поддерживает потоковые блоки использования при
обычном транспорте openai-completions, предпочитайте общие вспомогательные функции каталога из
openclaw/plugin-sdk/provider-catalog-shared вместо жёстко заданных
проверок идентификатора провайдера. supportsNativeStreamingUsageCompat(...) и
applyProviderNativeStreamingUsageCompat(...) определяют поддержку по
карте возможностей конечной точки, поэтому нативные конечные точки в стиле Moonshot/DashScope по-прежнему
могут включить эту возможность, даже если плагин использует пользовательский идентификатор провайдера.
Приведённые выше примеры динамического обнаружения охватывают API провайдеров в стиле /models. Выполняйте
такое обнаружение внутри catalog.run только при наличии пригодных данных аутентификации, а
staticRun оставляйте без сетевых операций для автономного формирования каталога.
Добавьте динамическое разрешение моделей
Если ваш провайдер принимает произвольные идентификаторы моделей (например, прокси или маршрутизатор),
добавьте 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 будет запущена снова после его завершения.
Добавьте хуки среды выполнения (при необходимости)
Большинству провайдеров требуются только catalog и resolveDynamicModel. Добавляйте хуки
постепенно, по мере возникновения требований у вашего провайдера.
Общие конструкторы вспомогательных функций теперь охватывают самые распространённые семейства совместимости воспроизведения и инструментов, поэтому плагинам обычно не требуется вручную подключать каждый хук по отдельности:
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,});Доступные на сегодняшний день семейства воспроизведения:
| Семейство | Что оно подключает | Встроенные примеры |
|---|---|---|
openai-compatible |
Общая политика воспроизведения в стиле OpenAI для транспортов, совместимых с OpenAI, включая очистку идентификаторов вызовов инструментов, исправление порядка с первым сообщением ассистента и общую проверку реплик Gemini там, где она необходима транспорту | moonshot, ollama, xai, zai |
anthropic-by-model |
Политика воспроизведения с учётом Claude, выбираемая через modelId, благодаря чему транспорты сообщений Anthropic получают очистку блоков рассуждения, специфичную для Claude, только когда разрешённая модель действительно имеет идентификатор Claude |
amazon-bedrock |
native-anthropic-by-model |
Та же политика Claude по модели, что и anthropic-by-model, а также очистка идентификаторов вызовов инструментов и сохранение нативных идентификаторов использования инструментов Anthropic для транспортов, которым необходимо сохранять нативные идентификаторы поставщика |
anthropic-vertex, clawrouter |
google-gemini |
Политика нативного воспроизведения Gemini вместе с очисткой начального воспроизведения. Общее семейство сохраняет для Gemini CLI с текстовым выводом рассуждения с тегами; прямой провайдер google переопределяет resolveReasoningOutputMode на native, поскольку рассуждения Gemini API поступают как нативные части мыслей. |
google, google-gemini-cli |
passthrough-gemini |
Очистка сигнатур мыслей Gemini для моделей Gemini, работающих через прокси-транспорты, совместимые с OpenAI; не включает нативную проверку воспроизведения Gemini или перезапись начальной загрузки | openrouter, kilocode, opencode, opencode-go |
hybrid-anthropic-openai |
Гибридная политика для провайдеров, которые объединяют поверхности моделей сообщений Anthropic и OpenAI-совместимых моделей в одном плагине; необязательное удаление блоков рассуждения только для Claude остаётся ограничено стороной Anthropic | minimax |
Доступные на сегодняшний день семейства потоковой передачи:
| Семейство | Что оно подключает | Встроенные примеры |
|---|---|---|
google-thinking |
Нормализация полезной нагрузки рассуждений Gemini в общем потоковом пути | google, google-gemini-cli |
kilocode-thinking |
Обёртка рассуждений Kilo в общем потоковом пути прокси, при этом kilo/auto и неподдерживаемые прокси-идентификаторы рассуждений пропускают внедрение рассуждений |
kilocode |
moonshot-thinking |
Сопоставление бинарной нативной полезной нагрузки рассуждений Moonshot из конфигурации и уровня /think |
moonshot |
minimax-fast-mode |
Перезапись модели быстрого режима MiniMax в общем потоковом пути | minimax, minimax-portal |
openai-responses-defaults |
Общие нативные обёртки OpenAI/Codex Responses: заголовки атрибуции, /fast/serviceTier, детализация текста, нативный веб-поиск Codex, формирование полезной нагрузки для совместимости рассуждений и управление контекстом Responses |
openai |
openrouter-thinking |
Обёртка рассуждений OpenRouter для прокси-маршрутов с централизованной обработкой пропусков для неподдерживаемых моделей/auto |
openrouter |
tool-stream-default-on |
Включённая по умолчанию обёртка tool_stream для таких провайдеров, как Z.AI, которым нужна потоковая передача инструментов, если она явно не отключена |
zai |
Точки расширения SDK, обеспечивающие работу конструкторов семейств
Каждый конструктор семейства состоит из общедоступных низкоуровневых вспомогательных функций, экспортируемых из того же пакета; их можно использовать, когда провайдеру требуется отклониться от общего шаблона:
openclaw/plugin-sdk/provider-model-shared—ProviderReplayFamily,buildProviderReplayFamilyHooks(...)и низкоуровневые конструкторы воспроизведения (buildOpenAICompatibleReplayPolicy,buildAnthropicReplayPolicyForModel,buildGoogleGeminiReplayPolicy,buildHybridAnthropicOrOpenAIReplayPolicy). Также экспортирует вспомогательные функции воспроизведения 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), очистка предварительного заполнения рассуждений Anthropic Messages (createAnthropicThinkingPrefillPayloadWrapper), совместимость вызовов инструментов в виде обычного текста (createPlainTextToolCallCompatWrapper) и общие обёртки прокси и провайдеров (createOpenRouterWrapper,createToolStreamWrapper,createMinimaxFastModeWrapper).openclaw/plugin-sdk/provider-stream-shared— лёгкие обёртки полезной нагрузки и событий для интенсивно используемых путей провайдеров, включаяcreateOpenAICompatibleCompletionsThinkingOffWrapper,createPayloadPatchStreamWrapper,createPlainTextToolCallCompatWrapper,normalizeOpenAICompatibleReasoningPayload(...)иsetQwenChatTemplateThinking(...).openclaw/plugin-sdk/provider-tools—ProviderToolCompatFamily,buildProviderToolCompatFamilyHooks("deepseek" | "gemini" | "openai")и базовые вспомогательные функции схем провайдеров.
Для провайдеров семейства Gemini согласуйте режим вывода рассуждений с
транспортом. Провайдеры прямого Google Gemini API должны использовать вывод рассуждений
native, чтобы OpenClaw обрабатывал нативные части мыслей без добавления
директив запросов <think> / <final>. Текстовые серверные части в стиле Gemini CLI,
которые анализируют итоговый ответ в формате JSON или текста, могут сохранять общий
контракт с тегами google-gemini.
Некоторые потоковые вспомогательные функции намеренно остаются локальными для провайдера. @openclaw/anthropic-provider сохраняет wrapAnthropicProviderStream, resolveAnthropicBetas, resolveAnthropicFastMode, resolveAnthropicServiceTier и низкоуровневые конструкторы обёрток Anthropic в собственной общедоступной точке расширения api.ts / contract-api.ts, поскольку они кодируют обработку бета-версии OAuth Claude и ограничение context1m. Плагин xAI аналогичным образом сохраняет формирование нативных Responses xAI в собственном wrapStreamFn (псевдонимы /fast, значение tool_stream по умолчанию, очистка неподдерживаемых строгих инструментов, специфичное для xAI удаление полезной нагрузки рассуждений).
Тот же шаблон корня пакета также лежит в основе @openclaw/openai-provider (конструкторы провайдеров, вспомогательные функции модели по умолчанию, конструкторы провайдеров реального времени) и @openclaw/openrouter-provider (конструктор провайдера вместе со вспомогательными функциями первоначальной настройки и конфигурации).
Обмен токенами
Для провайдеров, которым требуется обмен токенами перед каждым вызовом логического вывода:
prepareRuntimeAuth: async (ctx) => { const exchanged = await exchangeToken(ctx.apiKey); return { apiKey: exchanged.token, baseUrl: exchanged.baseUrl, expiresAt: exchanged.expiresAt, };},Пользовательские заголовки
Для провайдеров, которым требуются пользовательские заголовки запроса или изменения тела:
// 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); };},Идентификация нативного транспорта
Для провайдеров, которым требуются нативные заголовки запроса или сеанса либо метаданные в универсальных транспортах 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,}),Использование и тарификация
Для провайдеров, предоставляющих данные об использовании и тарификации:
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?, subscriptionType?, rateLimitTier? }, когда у
провайдера есть учётные данные для использования или выставления счетов (необязательные поля передают
несекретные метаданные плана из разрешённого профиля в
fetchUsageSnapshot). Возвращайте
{ handled: true } только тогда, когда провайдер окончательно обработал аутентификацию
для получения данных об использовании, но не имеет пригодного токена использования, и OpenClaw должен пропустить типовой
резервный механизм с API-ключом/OAuth. Верните null или undefined, когда провайдер
не обработал запрос и OpenClaw должен продолжить использовать типовой резервный механизм.
Объявите идентификатор провайдера в contracts.usageProviders. Когда этот контракт манифеста
и оба хука присутствуют, OpenClaw автоматически включает
провайдера в сбор данных об использовании, не загружая несвязанные плагины
провайдеров. Обновлять список разрешённых провайдеров в ядре не требуется.
fetchUsageSnapshot возвращает общую нейтральную к провайдеру структуру:
plan: указанная провайдером подписка или метка ключаwindows: окна квот со сбросом в виде процентов использованияbilling: типизированные записиbalance,spendилиbudget;unitможет быть кодом валюты ISO или единицей провайдера, напримерcreditssummary: компактный контекст конкретного провайдера, который не помещается в эти структурированные поля
Точно сохраняйте семантику валюты. Кредит провайдера не является суммой в USD, если
это явно не определено вышестоящим контрактом. Плагин, реализующий только
fetchUsageSnapshot, остаётся доступным для явных или синтетических вызывающих компонентов, но
не обнаруживается автоматически, поскольку OpenClaw не может разрешить его учётные данные для получения данных об использовании.
Распространённые хуки провайдеров
OpenClaw вызывает хуки плагинов моделей и провайдеров примерно в следующем порядке.
Большинство провайдеров используют только 2–3 из них. Это не полный контракт ProviderPlugin —
полный актуальный список хуков и примечания о резервных механизмах см. в разделе Внутреннее устройство: хуки среды выполнения
провайдера.
Поля провайдера, предназначенные только для совместимости и больше не вызываемые OpenClaw, например
ProviderPlugin.capabilities и suppressBuiltInModel, здесь
не перечислены.
| Хук | Когда использовать |
|---|---|
catalog |
Каталог моделей или значения базового URL по умолчанию |
applyConfigDefaults |
Глобальные значения по умолчанию, принадлежащие провайдеру, при материализации конфигурации |
normalizeModelId |
Очистка устаревших или предварительных псевдонимов идентификаторов моделей перед поиском |
normalizeTransport |
Очистка api / baseUrl семейства провайдеров перед типовой сборкой модели |
normalizeConfig |
Нормализация конфигурации models.providers.<id> |
applyNativeStreamingUsageCompat |
Преобразования совместимости для нативных данных об использовании при потоковой передаче у провайдеров конфигурации |
resolveConfigApiKey |
Разрешение аутентификации по маркерам переменных окружения, принадлежащее провайдеру |
resolveSyntheticAuth |
Синтетическая аутентификация для локального или самостоятельно размещённого провайдера либо на основе конфигурации |
resolveExternalAuthProfiles |
Наложение внешних профилей аутентификации, принадлежащих провайдеру, для учётных данных, управляемых CLI или приложением |
shouldDeferSyntheticProfileAuth |
Понижение приоритета синтетических заполнителей сохранённых профилей относительно аутентификации через окружение или конфигурацию |
resolveDynamicModel |
Приём произвольных идентификаторов моделей вышестоящего сервиса |
prepareDynamicModel |
Асинхронное получение метаданных перед разрешением |
normalizeResolvedModel |
Преобразования транспорта перед средой выполнения |
normalizeToolSchemas |
Очистка схемы инструментов, принадлежащая провайдеру, перед регистрацией |
inspectToolSchemas |
Диагностика схемы инструментов, принадлежащая провайдеру |
resolveReasoningOutputMode |
Контракт вывода рассуждений с тегами или в нативном формате |
prepareExtraParams |
Параметры запроса по умолчанию |
createStreamFn |
Полностью пользовательский транспорт StreamFn |
wrapStreamFn |
Пользовательские обёртки заголовков или тела на обычном потоковом пути |
resolveTransportTurnState |
Нативные заголовки или метаданные для каждого хода |
resolveWebSocketSessionPolicy |
Заголовки или период ожидания нативного сеанса WS |
formatApiKey |
Пользовательская структура токена среды выполнения |
refreshOAuth |
Пользовательское обновление OAuth |
buildAuthDoctorHint |
Рекомендации по исправлению аутентификации |
matchesContextOverflowError |
Обнаружение переполнения, принадлежащее провайдеру |
classifyFailoverReason |
Классификация ограничения частоты или перегрузки, принадлежащая провайдеру |
isCacheTtlEligible |
Ограничение по TTL кэша промпта |
buildMissingAuthMessage |
Пользовательская подсказка об отсутствии аутентификации |
augmentModelCatalog |
Синтетические строки прямой совместимости (устарело — предпочтительно registerModelCatalogProvider) |
resolveThinkingProfile |
Набор параметров /think для конкретной модели |
isBinaryThinking |
Совместимость с двоичным включением или отключением режима размышления (устарело — предпочтительно resolveThinkingProfile) |
supportsXHighThinking |
Совместимость с поддержкой рассуждений xhigh (устарело — предпочтительно resolveThinkingProfile) |
resolveDefaultThinkingLevel |
Совместимость с политикой /think по умолчанию (устарело — предпочтительно resolveThinkingProfile) |
isModernModelRef |
Сопоставление моделей для реальных или дымовых проверок |
prepareRuntimeAuth |
Обмен токена перед выполнением логического вывода |
resolveUsageAuth |
Пользовательский разбор учётных данных для получения данных об использовании |
fetchUsageSnapshot |
Пользовательская конечная точка данных об использовании |
createEmbeddingProvider |
Адаптер векторных представлений для памяти или поиска, принадлежащий провайдеру |
buildReplayPolicy |
Пользовательская политика повторного воспроизведения или Compaction транскрипта |
sanitizeReplayHistory |
Преобразования повторного воспроизведения для конкретного провайдера после типовой очистки |
validateReplayTurns |
Строгая проверка ходов повторного воспроизведения перед встроенной средой выполнения |
onModelSelected |
Обратный вызов после выбора (например, для телеметрии) |
Примечания о резервных механизмах среды выполнения:
normalizeConfigразрешает один владеющий плагин для каждого идентификатора провайдера (сначала встроенные провайдеры, затем соответствующий плагин среды выполнения) и вызывает только этот хук — сканирование других провайдеров не выполняется. Собственный хук GooglenormalizeConfigнормализует записи конфигурацииgoogle/google-vertex/google-antigravity; это не отдельный резервный механизм ядра.resolveConfigApiKeyиспользует хук провайдера, если тот предоставлен. Amazon Bedrock сохраняет разрешение маркеров переменных окружения AWS в своём плагине провайдера; сама аутентификация среды выполнения по-прежнему использует стандартную цепочку AWS SDK при настройке сauth: "aws-sdk".resolveThinkingProfile(ctx)получает выбранныеprovider,modelId, необязательную объединённую подсказку каталогаreasoningи необязательные объединённые сведения о моделиcompat. Используйтеcompatтолько для выбора пользовательского интерфейса или профиля размышления провайдера.resolveSystemPromptContributionпозволяет провайдеру внедрять учитывающие кэш рекомендации для системного промпта семейства моделей. Предпочитайте его устаревшему общему для всего плагина хукуbefore_prompt_build, когда поведение относится к одному провайдеру или семейству моделей и должно сохранять разделение кэша на стабильную и динамическую части.
Добавьте дополнительные возможности (необязательно)
Шаг 5. Добавьте дополнительные возможности
Плагин провайдера может регистрировать векторные представления, синтез речи, транскрибирование в реальном времени, голосовую связь в реальном времени, анализ мультимедиа, генерацию изображений, генерацию видео, получение веб-страниц и веб-поиск наряду с текстовым логическим выводом. OpenClaw классифицирует его как плагин с гибридными возможностями — это рекомендуемый шаблон для плагинов компаний (один плагин на поставщика). См. Внутреннее устройство: владение возможностями.
Зарегистрируйте каждую возможность внутри register(api) рядом с существующим
вызовом api.registerProvider(...). Выберите только нужные вкладки:
Речь (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 и
суффиксы идентификаторов запросов.
Транскрибирование в реальном времени
Предпочитайте createRealtimeTranscriptionWebSocketSession(...) — общий
вспомогательный компонент обрабатывает перехват прокси, задержку повторного подключения, сброс при закрытии, начальные
подтверждения готовности, постановку аудио в очередь и диагностику событий закрытия. Ваш плагин
только сопоставляет события вышестоящего сервиса.
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" }); }, }); },});Пакетные провайдеры STT, отправляющие аудио через POST в формате multipart, должны использовать
buildAudioTranscriptionFormData(...) из
openclaw/plugin-sdk/provider-http. Вспомогательная функция нормализует имена
загружаемых файлов, включая загрузки AAC, которым для совместимых API
транскрибирования требуется имя файла в стиле M4A.
Голос в реальном времени
api.registerRealtimeVoiceProvider({ id: "acme-ai", label: "Голос Acme в реальном времени", capabilities: { transports: ["gateway-relay"], inputAudioFormats: [{ encoding: "pcm16", sampleRateHz: 24000, channels: 1 }], outputAudioFormats: [{ encoding: "pcm16", sampleRateHz: 24000, channels: 1 }], supportsBargeIn: true, handlesInputAudioBargeIn: true, supportsToolCalls: true, }, isConfigured: ({ providerConfig }) => Boolean(providerConfig.apiKey), createBridge: (req) => ({ // Указывайте это только в том случае, если провайдер принимает несколько ответов инструмента // для одного вызова, например немедленный ответ "выполняется", за которым следует // окончательный результат. supportsToolResultContinuation: false, connect: async () => {}, sendAudio: () => {}, setMediaTimestamp: () => {}, handleBargeIn: () => {}, submitToolResult: () => {}, acknowledgeMark: () => {}, close: () => {}, isConnected: () => true, }),});Объявите capabilities, чтобы talk.catalog мог предоставлять допустимые режимы,
транспорты, аудиоформаты и флаги функций браузерным и нативным клиентам
Talk. Реализуйте handleBargeIn, если транспорт может определить, что
пользователь прерывает воспроизведение ответа ассистента, а провайдер поддерживает
сокращение или очистку активного аудиоответа.
submitToolResult может возвращать void для синхронной отправки или
Promise<void> для асинхронной границы завершения, которую может предоставить
мост провайдера. Сеансы ретрансляции Gateway ожидают выполнения этого промиса, прежде чем
подтвердить окончательный результат или очистить связанный запуск; отклоняйте его при
сбое отправки.
Установите supportsToolResultSuppression: false, если провайдер не может
соблюдать options.suppressResponse. Тогда OpenClaw не применяет подавление к
внутренним результатам принудительного обращения и отмены, а также отклоняет прямые
запросы подавленных результатов вместо неявного запуска ответа.
Потребители createRealtimeVoiceBridgeSession также могут возвращать
промис из onToolCall; синхронные исключения и отклонения направляются
в обратный вызов сеанса onError.
Устанавливайте handlesInputAudioBargeIn только тогда, когда VAD провайдера подтверждает
прерывание вызовом onClearAudio("barge-in"). Для провайдеров, которые не указывают
этот флаг, OpenClaw использует локальное резервное обнаружение по входному аудио.
Анализ медиаданных
api.registerMediaUnderstandingProvider({ id: "acme-ai", capabilities: ["image", "audio"], describeImage: async (req) => ({ text: "Фотография..." }), transcribeAudio: async (req) => ({ text: "Транскрипция..." }),});Локальные или самостоятельно размещённые медиапровайдеры, которым намеренно не требуются
учётные данные, могут предоставлять resolveAuth и возвращать kind: "none".
OpenClaw по-прежнему сохраняет обычную проверку аутентификации для провайдеров, которые
явно не включили эту возможность. Существующие провайдеры могут продолжать читать req.apiKey;
новым провайдерам следует предпочитать req.auth.
api.registerMediaUnderstandingProvider({ id: "local-audio", capabilities: ["audio"], resolveAuth: () => ({ kind: "none", source: "плагин local-audio без аутентификации", }), transcribeAudio: async (req) => ({ text: "Транскрипция..." }),});Эмбеддинги
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), ), ), }, }),});Объявите тот же идентификатор в contracts.embeddingProviders. Это
общий контракт эмбеддингов для многократно используемой генерации векторов, включая
поиск по памяти. registerMemoryEmbeddingProvider(...) — устаревший
механизм совместимости для существующих адаптеров, предназначенных для памяти.
Генерация изображений и видео
Возможности работы с изображениями и видео используют структуру с учётом режима. Провайдеры
изображений объявляют обязательные блоки возможностей generate и edit;
провайдеры видео объявляют generate, imageToVideo и
videoToVideo. Плоских агрегированных полей, таких как maxInputImages /
maxInputVideos / maxDurationSeconds, недостаточно для корректного объявления
поддержки режима преобразования или отключённых режимов. Генерация музыки
следует той же схеме generate / edit.
api.registerImageGenerationProvider({ id: "acme-ai", label: "Изображения Acme", capabilities: { generate: { maxCount: 4, supportsSize: true }, edit: { enabled: false }, }, generateImage: async (req) => ({ images: [] }),}); api.registerVideoGenerationProvider({ id: "acme-ai", label: "Видео Acme", defaultTimeoutMs: 600_000, models: ["acme-video", "acme-image-video"], 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 }, }, catalogByModel: { "acme-image-video": { modes: ["imageToVideo"], capabilities: { imageToVideo: { enabled: true, maxVideos: 1, maxInputImages: 1, resolutions: ["480P", "720P", "1080P"], supportsResolution: true, }, videoToVideo: { enabled: false }, }, }, }, generateVideo: async (req) => ({ videos: [] }),});capabilities обязателен для обоих типов провайдеров; edit и
блоки преобразования видео (imageToVideo, videoToVideo) всегда требуют
явного флага enabled.
Используйте catalogByModel, когда статические режимы или возможности указанной модели
отличаются от значений провайдера по умолчанию. Эти метаданные обеспечивают точность
video_generate action=list и каталогов моделей без
вызова кода провайдера. Поиск и проверка возможностей во время запроса
по-прежнему относятся к resolveModelCapabilities и generateVideo; по возможности
используйте одну и ту же константу возможностей для обоих путей.
Получение данных и поиск в интернете
api.registerWebFetchProvider({ id: "acme-ai-fetch", label: "Получение данных Acme", hint: "Получайте страницы через серверную часть рендеринга Acme.", 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: "Получить страницу через Acme Fetch.", parameters: {}, execute: async (args) => ({ content: [] }), }),}); api.registerWebSearchProvider({ id: "acme-ai-search", label: "Поиск Acme", hint: "Выполняйте поиск в интернете через серверную часть поиска Acme.", envVars: ["ACME_SEARCH_API_KEY"], placeholder: "acme-...", signupUrl: "https://acme.example.com/search", credentialPath: "plugins.entries.acme.config.webSearch.apiKey", getCredentialValue: (searchConfig) => searchConfig?.acme?.apiKey, setCredentialValue: (searchConfigTarget, value) => { const acme = (searchConfigTarget.acme ??= {}); acme.apiKey = value; }, createTool: () => ({ description: "Выполнить поиск в интернете через Acme Search.", parameters: {}, execute: async (args) => ({ content: [] }), }),});Оба типа провайдеров используют одну и ту же структуру подключения учётных данных:
hint, envVars, placeholder, signupUrl, credentialPath,
getCredentialValue, setCredentialValue и createTool —
обязательны.
Тестирование
Шаг 6: Тестирование
import { describe, it, expect } from "vitest";// Экспортируйте объект конфигурации провайдера из index.ts или отдельного файлаimport { acmeProvider } from "./provider.js"; describe("провайдер acme-ai", () => { it("разрешает динамические модели", () => { const model = acmeProvider.resolveDynamicModel!({ modelId: "acme-beta-v3", } as any); expect(model.id).toBe("acme-beta-v3"); expect(model.provider).toBe("acme-ai"); }); it("возвращает каталог при наличии ключа", async () => { const result = await acmeProvider.catalog!.run({ resolveProviderApiKey: () => ({ apiKey: "test-key" }), } as any); expect(result?.provider?.models).toHaveLength(2); }); it("возвращает пустой каталог при отсутствии ключа", async () => { const result = await acmeProvider.catalog!.run({ resolveProviderApiKey: () => ({ apiKey: undefined }), } as any); expect(result).toBeNull(); });});Публикация в ClawHub
Плагины провайдеров публикуются так же, как и любые другие внешние плагины с кодом:
clawhub package publish your-org/your-plugin --dry-runclawhub package publish your-org/your-pluginclawhub skill publish <path> — это другая команда для публикации папки навыка,
а не пакета плагина — не используйте её здесь.
Структура файлов
<bundled-plugin-root>/acme-ai/├── package.json # метаданные openclaw.providers├── openclaw.plugin.json # Манифест с метаданными аутентификации провайдера├── index.ts # definePluginEntry + registerProvider└── src/ ├── provider.test.ts # Тесты └── usage.ts # Конечная точка использования (необязательно)Справочник по порядку каталогов
catalog.order определяет, когда ваш каталог объединяется относительно встроенных
провайдеров:
| Порядок | Когда | Вариант использования |
|---|---|---|
simple |
Первый проход | Провайдеры только с API-ключом |
profile |
После простых | Провайдеры, требующие профилей аутентификации |
paired |
После профиля | Формирование нескольких связанных записей |
late |
Последний проход | Переопределение существующих провайдеров (при конфликте имеет приоритет) |
Дальнейшие действия
- Плагины каналов — если ваш плагин также предоставляет канал
- Среда выполнения SDK — вспомогательные функции
api.runtime(TTS, поиск, субагент) - Обзор SDK — полный справочник по импорту подпутей
- Внутреннее устройство плагинов — сведения о хуках и встроенные примеры