Building plugins
Créer des Plugins de fournisseurs
Ce guide explique comment créer un plugin fournisseur qui ajoute un fournisseur de modèles (LLM) à OpenClaw. À la fin, vous disposerez d’un fournisseur avec un catalogue de modèles, une authentification par clé API et une résolution dynamique des modèles.
Procédure pas à pas
Package et manifeste
Étape 1 : Package et manifeste
{"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}}Le manifeste déclare setup.providers[].envVars afin qu’OpenClaw puisse détecter
les identifiants sans charger l’exécution de votre plugin. Ajoutez providerAuthAliases
lorsqu’une variante de fournisseur doit réutiliser l’authentification de l’id d’un autre fournisseur. modelSupport
est facultatif et permet à OpenClaw de charger automatiquement votre plugin fournisseur à partir d’identifiants
de modèle abrégés comme acme-large avant que les hooks d’exécution existent. Si vous publiez le
fournisseur sur ClawHub, ces champs openclaw.compat et openclaw.build
sont requis dans package.json.
Enregistrer le fournisseur
Un fournisseur de texte minimal nécessite un id, un label, une auth et un catalog.
catalog est le hook d’exécution/configuration détenu par le fournisseur ; il peut appeler des
API fournisseur en direct et retourne des entrées 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 est la nouvelle surface de catalogue du plan de contrôle
pour l’interface de liste/aide/sélection. Utilisez-la pour les lignes de texte, de génération d’images,
de génération de vidéos et de génération de musique. Gardez les appels aux points de terminaison fournisseur et
le mappage des réponses dans le plugin ; OpenClaw possède la forme de ligne partagée, les libellés
de source et le rendu de l’aide.
Il s’agit d’un fournisseur fonctionnel. Les utilisateurs peuvent maintenant exécuter
openclaw onboard --acme-ai-api-key <key> et sélectionner
acme-ai/acme-large comme modèle.
Découverte de modèles en direct
Si votre fournisseur expose une API de type /models, conservez le point de terminaison
spécifique au fournisseur et la projection des lignes dans votre plugin, puis utilisez
openclaw/plugin-sdk/provider-catalog-live-runtime pour le cycle de vie de récupération partagé.
L’assistant vous donne des récupérations HTTP protégées, des en-têtes d’authentification fournisseur,
des erreurs HTTP structurées, une mise en cache TTL et un comportement de repli statique sans
placer la politique fournisseur dans le cœur d’OpenClaw.
Utilisez buildLiveModelProviderConfig lorsque l’API en direct vous indique uniquement quelles
lignes du catalogue statique détenu par le fournisseur sont actuellement disponibles :
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], }, }), }, }); },});Utilisez getCachedLiveProviderModelRows lorsque l’API du fournisseur retourne des
métadonnées plus riches et que le plugin doit lui-même projeter les lignes dans les
définitions de modèles 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 doit rester protégé par l’authentification et retourner null lorsqu’aucun identifiant utilisable
n’est disponible. Conservez un staticRun hors ligne ou un repli statique afin que les surfaces de configuration,
de documentation, de tests et de sélection ne dépendent pas d’un accès réseau en direct. Utilisez un TTL
adapté à la fraîcheur de la liste des modèles, évitez l’interrogation du système de fichiers au moment des requêtes,
et passez un readRows / readModelId spécifique au fournisseur uniquement lorsque la
réponse en amont n’est pas une forme compatible OpenAI { data: [{ id, object }] }.
Si le fournisseur en amont utilise des jetons de contrôle différents de ceux d’OpenClaw, ajoutez une petite transformation de texte bidirectionnelle au lieu de remplacer le chemin de flux :
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 réécrit le prompt système final et le contenu des messages texte avant
le transport. output réécrit les deltas de texte de l’assistant et le texte final avant
qu’OpenClaw analyse ses propres marqueurs de contrôle ou la livraison au canal.
Pour les fournisseurs groupés qui enregistrent uniquement un fournisseur de texte avec une authentification
par clé API et une seule exécution adossée à un catalogue, préférez l’assistant plus étroit
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 est le chemin de catalogue en direct utilisé quand OpenClaw peut résoudre une véritable
authentification de fournisseur. Il peut effectuer une découverte propre au fournisseur. Utilisez
buildStaticProvider uniquement pour les lignes hors ligne qui peuvent être affichées sans risque avant que l’authentification
soit configurée ; il ne doit pas exiger d’identifiants ni effectuer de requêtes réseau.
L’affichage models list --all d’OpenClaw exécute actuellement les catalogues statiques
uniquement pour les plugins de fournisseurs intégrés, avec une configuration vide, un environnement vide et aucun
chemin d’agent/espace de travail.
Si votre flux d’authentification doit aussi corriger models.providers.*, les alias et
le modèle par défaut de l’agent pendant l’onboarding, utilisez les assistants de préréglage de
openclaw/plugin-sdk/provider-onboard. Les assistants les plus ciblés sont
createDefaultModelPresetAppliers(...),
createDefaultModelsPresetAppliers(...) et
createModelCatalogPresetAppliers(...).
Quand l’endpoint natif d’un fournisseur prend en charge les blocs d’utilisation en streaming sur le
transport openai-completions normal, préférez les assistants de catalogue partagés dans
openclaw/plugin-sdk/provider-catalog-shared plutôt que de coder en dur des vérifications
d’identifiant de fournisseur. supportsNativeStreamingUsageCompat(...) et
applyProviderNativeStreamingUsageCompat(...) détectent la prise en charge à partir de la
carte des capacités d’endpoint, de sorte que les endpoints natifs de style Moonshot/DashScope
s’activent toujours même lorsqu’un plugin utilise un identifiant de fournisseur personnalisé.
Les exemples de découverte en direct ci-dessus couvrent les API de fournisseur de style /models. Gardez
cette découverte dans catalog.run, protégée par une authentification utilisable, et gardez
staticRun sans réseau pour la génération de catalogue hors ligne.
Ajouter la résolution dynamique de modèle
Si votre fournisseur accepte des ID de modèle arbitraires (comme un proxy ou un routeur),
ajoutez 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, }),});Si la résolution nécessite un appel réseau, utilisez prepareDynamicModel pour un préchauffage
asynchrone - resolveDynamicModel s’exécute de nouveau une fois celui-ci terminé.
Ajouter des hooks d’exécution (si nécessaire)
La plupart des fournisseurs n’ont besoin que de catalog + resolveDynamicModel. Ajoutez des hooks
progressivement, selon les besoins de votre fournisseur.
Les constructeurs d’assistants partagés couvrent désormais les familles de replay/compatibilité d’outils les plus courantes, de sorte que les plugins n’ont généralement pas besoin de câbler chaque hook un par un :
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,});Familles de replay disponibles aujourd’hui :
| Famille | Ce que cela connecte | Exemples intégrés |
|---|---|---|
openai-compatible |
Politique de replay partagée de style OpenAI pour les transports compatibles OpenAI, incluant l’assainissement des ID d’appels d’outils, les corrections d’ordre assistant-first et la validation générique des tours Gemini là où le transport en a besoin | moonshot, ollama, xai, zai |
anthropic-by-model |
Politique de replay compatible Claude choisie par modelId, afin que les transports de messages Anthropic n’obtiennent le nettoyage des blocs de réflexion propre à Claude que lorsque le modèle résolu est réellement un ID Claude |
amazon-bedrock, anthropic-vertex |
google-gemini |
Politique de replay Gemini native plus assainissement du replay d’amorçage. La famille partagée garde le CLI Gemini à sortie texte sur le raisonnement balisé ; le fournisseur direct google remplace resolveReasoningOutputMode par native, car la réflexion de l’API Gemini arrive sous forme de parties de pensée natives. |
google, google-gemini-cli |
passthrough-gemini |
Assainissement des signatures de pensée Gemini pour les modèles Gemini exécutés via des transports proxy compatibles OpenAI ; n’active pas la validation native de replay Gemini ni les réécritures d’amorçage | openrouter, kilocode, opencode, opencode-go |
hybrid-anthropic-openai |
Politique hybride pour les fournisseurs qui mélangent des surfaces de modèles de messages Anthropic et compatibles OpenAI dans un même plugin ; la suppression optionnelle des blocs de réflexion réservée à Claude reste limitée au côté Anthropic | minimax |
Familles de flux disponibles aujourd’hui :
| Famille | Ce que cela connecte | Exemples intégrés |
|---|---|---|
google-thinking |
Normalisation de la charge utile de réflexion Gemini sur le chemin de flux partagé | google, google-gemini-cli |
kilocode-thinking |
Enveloppe de raisonnement Kilo sur le chemin de flux proxy partagé, avec kilo/auto et les ID de raisonnement proxy non pris en charge ignorant la réflexion injectée |
kilocode |
moonshot-thinking |
Mappage de charge utile native-thinking binaire Moonshot depuis la configuration + le niveau /think |
moonshot |
minimax-fast-mode |
Réécriture de modèle en mode rapide MiniMax sur le chemin de flux partagé | minimax, minimax-portal |
openai-responses-defaults |
Enveloppes Responses natives OpenAI/Codex partagées : en-têtes d’attribution, /fast/serviceTier, verbosité du texte, recherche web native Codex, mise en forme de charge utile compatible raisonnement et gestion du contexte Responses |
openai |
openrouter-thinking |
Enveloppe de raisonnement OpenRouter pour les routes proxy, avec les exclusions de modèles non pris en charge/auto gérées de manière centralisée |
openrouter |
tool-stream-default-on |
Enveloppe tool_stream activée par défaut pour les fournisseurs comme Z.AI qui veulent le streaming d’outils sauf désactivation explicite |
zai |
Seams SDK qui alimentent les constructeurs de familles
Chaque constructeur de famille est composé à partir d’assistants publics de plus bas niveau exportés par le même package, que vous pouvez utiliser quand un fournisseur doit sortir du schéma commun :
openclaw/plugin-sdk/provider-model-shared-ProviderReplayFamily,buildProviderReplayFamilyHooks(...)et les constructeurs de replay bruts (buildOpenAICompatibleReplayPolicy,buildAnthropicReplayPolicyForModel,buildGoogleGeminiReplayPolicy,buildHybridAnthropicOrOpenAIReplayPolicy). Exporte aussi les assistants de replay Gemini (sanitizeGoogleGeminiReplayHistory,resolveTaggedReasoningOutputMode) et les assistants d’endpoint/modèle (resolveProviderEndpoint,normalizeProviderId,normalizeGooglePreviewModelId).openclaw/plugin-sdk/provider-stream-ProviderStreamFamily,buildProviderStreamFamilyHooks(...),composeProviderStreamWrappers(...), ainsi que les enveloppes OpenAI/Codex partagées (createOpenAIAttributionHeadersWrapper,createOpenAIFastModeWrapper,createOpenAIServiceTierWrapper,createOpenAIResponsesContextManagementWrapper,createCodexNativeWebSearchWrapper), l’enveloppe compatible OpenAI DeepSeek V4 (createDeepSeekV4OpenAICompatibleThinkingWrapper), le nettoyage de préremplissage de réflexion Anthropic Messages (createAnthropicThinkingPrefillPayloadWrapper), la compatibilité d’appels d’outils en texte brut (createPlainTextToolCallCompatWrapper) et les enveloppes proxy/fournisseur partagées (createOpenRouterWrapper,createToolStreamWrapper,createMinimaxFastModeWrapper).openclaw/plugin-sdk/provider-stream-shared- enveloppes légères de charge utile et d’événements pour les chemins de fournisseurs critiques, incluantcreateOpenAICompatibleCompletionsThinkingOffWrapper,createPayloadPatchStreamWrapper,createPlainTextToolCallCompatWrapper,normalizeOpenAICompatibleReasoningPayload(...)etsetQwenChatTemplateThinking(...).openclaw/plugin-sdk/provider-tools-ProviderToolCompatFamily,buildProviderToolCompatFamilyHooks("deepseek" | "gemini" | "openai")et les assistants de schéma de fournisseur sous-jacents.
Pour les fournisseurs de la famille Gemini, gardez le mode de sortie de raisonnement aligné sur
le transport. Les fournisseurs directs de l’API Google Gemini doivent utiliser une sortie de raisonnement
native, afin qu’OpenClaw consomme les parties de pensée natives sans ajouter de directives de prompt
<think> / <final>. Les backends de style CLI Gemini à texte seul
qui analysent une réponse finale JSON/texte peuvent conserver le contrat balisé
google-gemini partagé.
Certains assistants de flux restent volontairement locaux au fournisseur. @openclaw/anthropic-provider garde wrapAnthropicProviderStream, resolveAnthropicBetas, resolveAnthropicFastMode, resolveAnthropicServiceTier et les constructeurs d’enveloppes Anthropic de plus bas niveau dans son propre seam public api.ts / contract-api.ts, car ils encodent la gestion des bêtas OAuth Claude et le contrôle de context1m. Le plugin xAI garde de même la mise en forme native xAI Responses dans son propre wrapStreamFn (alias /fast, tool_stream par défaut, nettoyage strict-tool non pris en charge, suppression de charge utile de raisonnement propre à xAI).
Le même schéma de racine de package prend aussi en charge @openclaw/openai-provider (constructeurs de fournisseur, assistants de modèle par défaut, constructeurs de fournisseur realtime) et @openclaw/openrouter-provider (constructeur de fournisseur plus assistants d’onboarding/configuration).
Échange de jeton
Pour les fournisseurs qui ont besoin d’un échange de jeton avant chaque appel d’inférence :
prepareRuntimeAuth: async (ctx) => { const exchanged = await exchangeToken(ctx.apiKey); return { apiKey: exchanged.token, baseUrl: exchanged.baseUrl, expiresAt: exchanged.expiresAt, };},En-têtes personnalisés
Pour les fournisseurs qui ont besoin d’en-têtes de requête personnalisés ou de modifications du corps :
// 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); };},Identité de transport native
Pour les fournisseurs qui ont besoin d’en-têtes de requête/session natifs ou de métadonnées sur des transports HTTP ou WebSocket génériques :
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,}),Utilisation et facturation
Pour les fournisseurs qui exposent des données d'utilisation/facturation :
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 a trois résultats possibles. Retournez { token, accountId? }
lorsque le fournisseur dispose d'un identifiant d'utilisation/facturation. Retournez
{ handled: true } uniquement lorsque le fournisseur a définitivement géré
l'authentification d'utilisation, mais n'a aucun jeton d'utilisation exploitable,
et qu'OpenClaw doit ignorer la solution de repli générique par clé d'API/OAuth.
Retournez null ou undefined lorsque le fournisseur n'a pas
traité la requête et qu'OpenClaw doit poursuivre avec la solution de repli générique.
Tous les hooks de fournisseur disponibles
OpenClaw appelle les hooks dans cet ordre. La plupart des fournisseurs n'en utilisent que 2 ou 3 :
les champs de fournisseur réservés à la compatibilité qu'OpenClaw n'appelle plus, tels que
ProviderPlugin.capabilities et suppressBuiltInModel, ne sont pas répertoriés
ici.
| # | Hook | Quand l'utiliser |
|---|---|---|
| 1 | catalog |
Catalogue de modèles ou valeurs par défaut de l'URL de base |
| 2 | applyConfigDefaults |
Valeurs globales par défaut détenues par le fournisseur lors de la matérialisation de la configuration |
| 3 | normalizeModelId |
Nettoyage des alias d'ID de modèle hérités/en préversion avant la recherche |
| 4 | normalizeTransport |
Nettoyage de api / baseUrl pour une famille de fournisseurs avant l'assemblage générique du modèle |
| 5 | normalizeConfig |
Normaliser la configuration models.providers.<id> |
| 6 | applyNativeStreamingUsageCompat |
Réécritures de compatibilité d'utilisation en streaming natif pour les fournisseurs de configuration |
| 7 | resolveConfigApiKey |
Résolution d'authentification par marqueur d'environnement détenue par le fournisseur |
| 8 | resolveSyntheticAuth |
Authentification synthétique locale/auto-hébergée ou adossée à la configuration |
| 9 | shouldDeferSyntheticProfileAuth |
Abaisser les espaces réservés de profils stockés synthétiques derrière l'authentification par environnement/configuration |
| 10 | resolveDynamicModel |
Accepter des ID de modèle amont arbitraires |
| 11 | prepareDynamicModel |
Récupération asynchrone des métadonnées avant la résolution |
| 12 | normalizeResolvedModel |
Réécritures de transport avant l'exécuteur |
| 13 | normalizeToolSchemas |
Nettoyage des schémas d'outils détenu par le fournisseur avant l'enregistrement |
| 14 | inspectToolSchemas |
Diagnostics des schémas d'outils détenus par le fournisseur |
| 15 | resolveReasoningOutputMode |
Contrat de sortie de raisonnement balisée ou native |
| 16 | prepareExtraParams |
Paramètres de requête par défaut |
| 17 | createStreamFn |
Transport StreamFn entièrement personnalisé |
| 19 | wrapStreamFn |
Enveloppes d'en-têtes/corps personnalisées sur le chemin de flux normal |
| 20 | resolveTransportTurnState |
En-têtes/métadonnées natifs par tour |
| 21 | resolveWebSocketSessionPolicy |
En-têtes de session WS natifs/temps de récupération |
| 22 | formatApiKey |
Forme personnalisée du jeton d'exécution |
| 23 | refreshOAuth |
Actualisation OAuth personnalisée |
| 24 | buildAuthDoctorHint |
Conseils de réparation d'authentification |
| 25 | matchesContextOverflowError |
Détection de dépassement détenue par le fournisseur |
| 26 | classifyFailoverReason |
Classification des limites de débit/surcharges détenue par le fournisseur |
| 27 | isCacheTtlEligible |
Contrôle du TTL du cache de prompts |
| 28 | buildMissingAuthMessage |
Indication personnalisée d'authentification manquante |
| 29 | augmentModelCatalog |
Lignes synthétiques de compatibilité ascendante |
| 30 | resolveThinkingProfile |
Ensemble d'options /think propre au modèle |
| 31 | isBinaryThinking |
Compatibilité de réflexion binaire activée/désactivée |
| 32 | supportsXHighThinking |
Compatibilité de prise en charge du raisonnement xhigh |
| 33 | resolveDefaultThinkingLevel |
Compatibilité de la stratégie /think par défaut |
| 34 | isModernModelRef |
Correspondance de modèle en direct/smoke |
| 35 | prepareRuntimeAuth |
Échange de jeton avant l'inférence |
| 36 | resolveUsageAuth |
Analyse personnalisée des identifiants d'utilisation |
| 37 | fetchUsageSnapshot |
Point de terminaison d'utilisation personnalisé |
| 38 | createEmbeddingProvider |
Adaptateur d'embeddings détenu par le fournisseur pour la mémoire/recherche |
| 39 | buildReplayPolicy |
Stratégie personnalisée de relecture/compaction de transcript |
| 40 | sanitizeReplayHistory |
Réécritures de relecture propres au fournisseur après le nettoyage générique |
| 41 | validateReplayTurns |
Validation stricte des tours de relecture avant l'exécuteur intégré |
| 42 | onModelSelected |
Rappel après sélection, par exemple pour la télémétrie |
Notes sur la solution de repli du runtime :
normalizeConfigvérifie d'abord le fournisseur correspondant, puis les autres plugins de fournisseur capables d'exposer ce hook jusqu'à ce que l'un d'eux modifie réellement la configuration. Si aucun hook de fournisseur ne réécrit une entrée de configuration prise en charge de la famille Google, le normalisateur de configuration Google intégré s'applique quand même.resolveConfigApiKeyutilise le hook du fournisseur lorsqu'il est exposé. Amazon Bedrock conserve la résolution des marqueurs d'environnement AWS dans son plugin de fournisseur ; l'authentification du runtime elle-même utilise toujours la chaîne par défaut du SDK AWS lorsqu'elle est configurée avecauth: "aws-sdk".resolveThinkingProfile(ctx)reçoit leprovidersélectionné,modelId, l'indication facultative de cataloguereasoningfusionnée, et les faitscompatfacultatifs du modèle fusionnés. Utilisezcompatuniquement pour sélectionner l'interface ou le profil de réflexion du fournisseur.resolveSystemPromptContributionpermet à un fournisseur d'injecter des conseils de prompt système compatibles avec le cache pour une famille de modèles. Préférez-le àbefore_prompt_buildlorsque le comportement appartient à un fournisseur ou une famille de modèles et doit préserver la séparation stable/dynamique du cache.
Pour des descriptions détaillées et des exemples concrets, consultez Internes : hooks de runtime des fournisseurs.
Ajouter des capacités supplémentaires (facultatif)
Étape 5 : Ajouter des capacités supplémentaires
Un plugin de fournisseur peut enregistrer des embeddings, la synthèse vocale, la transcription en temps réel, la voix en temps réel, la compréhension des médias, la génération d'images, la génération de vidéos, la récupération web et la recherche web en plus de l'inférence de texte. OpenClaw classe cela comme un plugin à capacité hybride - le modèle recommandé pour les plugins d'entreprise (un plugin par fournisseur). Consultez Internes : propriété des capacités.
Enregistrez chaque capacité dans register(api) à côté de votre appel
api.registerProvider(...) existant. Choisissez uniquement les onglets dont vous avez besoin :
Synthèse vocale (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(); } },});Utilisez assertOkOrThrowProviderError(...) pour les échecs HTTP du fournisseur afin que
les plugins partagent des lectures plafonnées des corps d'erreur, l'analyse des erreurs JSON et
les suffixes d'ID de requête.
Transcription en temps réel
Préférez createRealtimeTranscriptionWebSocketSession(...) - l'assistant partagé
gère la capture par proxy, le délai de reconnexion, le vidage à la fermeture, les poignées de main
de disponibilité, la mise en file d'attente audio et les diagnostics d'événements de fermeture. Votre plugin
ne fait que mapper les événements amont.
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" }); }, }); },});Les fournisseurs STT par lot qui envoient de l'audio multipart via POST doivent utiliser
buildAudioTranscriptionFormData(...) depuis
openclaw/plugin-sdk/provider-http. L'assistant normalise les noms de fichiers téléversés,
y compris les téléversements AAC qui nécessitent un nom de fichier de style M4A pour
les API de transcription compatibles.
Voix en temps réel
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, }),});Déclarez capabilities afin que talk.catalog puisse exposer les modes,
transports, formats audio et indicateurs de fonctionnalités valides aux clients Talk
navigateur et natifs. Implémentez handleBargeIn lorsqu’un transport peut détecter qu’un
humain interrompt la lecture de l’assistant et que le fournisseur prend en charge
la troncature ou l’effacement de la réponse audio active.
Media understanding
api.registerMediaUnderstandingProvider({ id: "acme-ai", capabilities: ["image", "audio"], describeImage: async (req) => ({ text: "A photo of..." }), transcribeAudio: async (req) => ({ text: "Transcript..." }),});Les fournisseurs multimédias locaux ou auto-hébergés qui, intentionnellement, ne nécessitent pas
d’identifiants peuvent exposer resolveAuth et renvoyer kind: "none".
OpenClaw conserve tout de même le contrôle d’authentification normal pour les fournisseurs qui ne
choisissent pas explicitement cette option. Les fournisseurs existants peuvent continuer à lire req.apiKey ;
les nouveaux fournisseurs devraient préférer 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), ), ), }, }),});Déclarez le même identifiant dans contracts.embeddingProviders. Il s’agit du
contrat général d’embeddings pour la génération réutilisable de vecteurs, y compris
la recherche en mémoire. registerMemoryEmbeddingProvider(...) est une compatibilité obsolète
pour les adaptateurs existants propres à la mémoire.
Image and video generation
Les capacités vidéo utilisent une forme sensible au mode : generate,
imageToVideo et videoToVideo. Les champs agrégés plats comme
maxInputImages / maxInputVideos / maxDurationSeconds ne suffisent pas
pour annoncer proprement la prise en charge des modes de transformation ou les modes désactivés.
La génération musicale suit le même modèle avec des blocs explicites 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: [] }),});Web fetch and search
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: [] }),});Test
Étape 6 : tester
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(); });});Publier sur ClawHub
Les Plugins de fournisseurs se publient de la même manière que n’importe quel autre Plugin de code externe :
clawhub package publish your-org/your-plugin --dry-runclawhub package publish your-org/your-pluginN’utilisez pas ici l’ancien alias de publication réservé aux Skills ; les packages de Plugin doivent utiliser
clawhub package publish.
Structure des fichiers
<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)Référence d’ordre du catalogue
catalog.order contrôle le moment où votre catalogue est fusionné par rapport aux fournisseurs
intégrés :
| Ordre | Quand | Cas d’utilisation |
|---|---|---|
simple |
Premier passage | Fournisseurs simples à clé API |
profile |
Après simple | Fournisseurs soumis aux profils d’authentification |
paired |
Après profile | Synthétiser plusieurs entrées liées |
late |
Dernier passage | Remplacer des fournisseurs existants (gagne en cas de collision) |
Prochaines étapes
- Plugins de canaux - si votre Plugin fournit également un canal
- SDK Runtime - assistants
api.runtime(TTS, recherche, sous-agent) - Vue d’ensemble du SDK - référence complète des imports de sous-chemins
- Internes du Plugin - détails des hooks et exemples intégrés