Naar hoofdinhoud gaan

Documentation Index

Fetch the complete documentation index at: https://docs2.openclaw.ai/llms.txt

Use this file to discover all available pages before exploring further.

Deze gids leidt je door het bouwen van een provider-Plugin die een modelprovider (LLM) aan OpenClaw toevoegt. Aan het einde heb je een provider met een modelcatalogus, API-sleutelauthenticatie en dynamische modelresolutie.
Als je nog niet eerder een OpenClaw-Plugin hebt gebouwd, lees dan eerst Aan de slag voor de basisstructuur van het pakket en de manifestconfiguratie.
Provider-Plugins voegen modellen toe aan de normale inferentielus van OpenClaw. Als het model via een native agent-daemon moet draaien die eigenaar is van threads, Compaction of tool- events, combineer de provider dan met een agent-harnas in plaats van daemonprotocoldetails in de core te plaatsen.

Stapsgewijze handleiding

1

Pakket en manifest

Stap 1: Pakket en manifest

{
  "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"
    }
  }
}
Het manifest declareert providerAuthEnvVars zodat OpenClaw inloggegevens kan detecteren zonder de runtime van je Plugin te laden. Voeg providerAuthAliases toe wanneer een providervariant de auth van een andere provider-id moet hergebruiken. modelSupport is optioneel en laat OpenClaw je provider-Plugin automatisch laden vanuit verkorte model-id’s zoals acme-large voordat runtime-hooks bestaan. Als je de provider op ClawHub publiceert, zijn die velden openclaw.compat en openclaw.build verplicht in package.json.
2

De provider registreren

Een minimale tekstprovider heeft een id, label, auth en catalog nodig. catalog is de runtime-/configuratiehook die eigendom is van de provider; deze kan live vendor-API’s aanroepen en retourneert models.providers-items.
index.ts
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 is het nieuwere control-plane-catalogusoppervlak voor UI voor lijsten/help/kiezers. Gebruik het voor rijen voor tekst, afbeeldingsgeneratie, videogeneratie en muziekgeneratie. Houd aanroepen naar vendor-eindpunten en responsmapping in de Plugin; OpenClaw is eigenaar van de gedeelde rijvorm, bronlabels en helpweergave.Dat is een werkende provider. Gebruikers kunnen nu openclaw onboard --acme-ai-api-key <key> uitvoeren en acme-ai/acme-large als hun model selecteren.Als de upstream-provider andere controletokens gebruikt dan OpenClaw, voeg dan een kleine bidirectionele teksttransformatie toe in plaats van het streampad te vervangen:
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 herschrijft de uiteindelijke systeemprompt en tekstberichtinhoud vóór transport. output herschrijft assistent-tekstdelta’s en definitieve tekst voordat OpenClaw zijn eigen controlemarkeringen of kanaalbezorging parseert.Voor gebundelde providers die slechts één tekstprovider registreren met API-sleutel- auth plus één door de catalogus ondersteunde runtime, geef de voorkeur aan de smallere 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 is het live-cataloguspad dat wordt gebruikt wanneer OpenClaw echte providerauth kan oplossen. Het mag provider-specifieke ontdekking uitvoeren. Gebruik buildStaticProvider alleen voor offline rijen die veilig zijn om te tonen voordat auth is geconfigureerd; het mag geen inloggegevens vereisen of netwerkverzoeken doen. De weergave models list --all van OpenClaw voert momenteel statische catalogi alleen uit voor gebundelde provider-Plugins, met een lege config, lege env en zonder agent-/workspacepaden.Als je auth-flow ook models.providers.*, aliassen en het standaardmodel van de agent tijdens onboarding moet patchen, gebruik dan de presethelpers uit openclaw/plugin-sdk/provider-onboard. De smalste helpers zijn createDefaultModelPresetAppliers(...), createDefaultModelsPresetAppliers(...) en createModelCatalogPresetAppliers(...).Wanneer het native eindpunt van een provider gestreamde gebruiksblokken op het normale openai-completions-transport ondersteunt, geef dan de voorkeur aan de gedeelde catalogushelpers in openclaw/plugin-sdk/provider-catalog-shared in plaats van provider-id-controles hard te coderen. supportsNativeStreamingUsageCompat(...) en applyProviderNativeStreamingUsageCompat(...) detecteren ondersteuning vanuit de capability-map van het eindpunt, zodat native Moonshot-/DashScope-achtige eindpunten zich nog steeds aanmelden, zelfs wanneer een Plugin een aangepaste provider-id gebruikt.
3

Dynamische modelresolutie toevoegen

Als je provider willekeurige model-ID’s accepteert (zoals een proxy of router), voeg dan resolveDynamicModel toe:
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,
  }),
});
Als oplossen een netwerkaanroep vereist, gebruik dan prepareDynamicModel voor asynchrone opwarming - resolveDynamicModel wordt opnieuw uitgevoerd nadat dit is voltooid.
4

Runtime-hooks toevoegen (indien nodig)

De meeste providers hebben alleen catalog + resolveDynamicModel nodig. Voeg hooks stapsgewijs toe wanneer je provider ze vereist.Gedeelde helperbuilders dekken nu de meest voorkomende replay-/toolcompatibiliteits- families, zodat Plugins meestal niet elke hook één voor één met de hand hoeven te verbinden:
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,
});
Beschikbare replayfamilies vandaag:
FamilieWat het koppeltMeegeleverde voorbeelden
openai-compatibleGedeeld OpenAI-achtig replaybeleid voor OpenAI-compatibele transporten, inclusief opschoning van tool-call-id’s, oplossingen voor assistant-first-volgorde en generieke Gemini-turn-validatie waar het transport dit nodig heeftmoonshot, ollama, xai, zai
anthropic-by-modelClaude-bewust replaybeleid gekozen door modelId, zodat Anthropic-message-transporten alleen Claude-specifieke opschoning van thinking-blokken krijgen wanneer het opgeloste model daadwerkelijk een Claude-id isamazon-bedrock, anthropic-vertex
google-geminiNative Gemini-replaybeleid plus opschoning van bootstrap-replay en modus voor getagde reasoning-uitvoergoogle, google-gemini-cli
passthrough-geminiOpschoning van Gemini-thought-signatures voor Gemini-modellen die via OpenAI-compatibele proxytransporten draaien; schakelt geen native Gemini-replayvalidatie of bootstrap-herschrijvingen inopenrouter, kilocode, opencode, opencode-go
hybrid-anthropic-openaiHybride beleid voor providers die Anthropic-message- en OpenAI-compatibele modeloppervlakken in één plugin combineren; optioneel verwijderen van thinking-blokken alleen voor Claude blijft beperkt tot de Anthropic-kantminimax
Vandaag beschikbare streamfamilies:
FamilieWat het koppeltMeegeleverde voorbeelden
google-thinkingNormalisatie van Gemini-thinking-payloads op het gedeelde streampadgoogle, google-gemini-cli
kilocode-thinkingKilo-reasoning-wrapper op het gedeelde proxy-streampad, waarbij kilo/auto en niet-ondersteunde proxy-reasoning-id’s geïnjecteerde thinking overslaankilocode
moonshot-thinkingMoonshot binaire native-thinking-payloadmapping vanuit config + /think-niveaumoonshot
minimax-fast-modeMiniMax fast-mode-modelherschrijving op het gedeelde streampadminimax, minimax-portal
openai-responses-defaultsGedeelde native OpenAI/Codex Responses-wrappers: attributieheaders, /fast/serviceTier, tekstuitgebreidheid, native Codex-webzoekactie, reasoning-compat-payloadvorming en Responses-contextbeheeropenai, openai-codex
openrouter-thinkingOpenRouter-reasoning-wrapper voor proxyroutes, waarbij overslaan voor niet-ondersteunde modellen/auto centraal wordt afgehandeldopenrouter
tool-stream-default-onStandaard ingeschakelde tool_stream-wrapper voor providers zoals Z.AI die toolstreaming willen tenzij dit expliciet is uitgeschakeldzai
Elke familiebouwer is samengesteld uit lagere openbare helpers die uit hetzelfde pakket worden geëxporteerd en die je kunt gebruiken wanneer een provider van het gemeenschappelijke patroon moet afwijken:
  • openclaw/plugin-sdk/provider-model-shared - ProviderReplayFamily, buildProviderReplayFamilyHooks(...) en de onbewerkte replaybouwers (buildOpenAICompatibleReplayPolicy, buildAnthropicReplayPolicyForModel, buildGoogleGeminiReplayPolicy, buildHybridAnthropicOrOpenAIReplayPolicy). Exporteert ook Gemini-replayhelpers (sanitizeGoogleGeminiReplayHistory, resolveTaggedReasoningOutputMode) en endpoint-/modelhelpers (resolveProviderEndpoint, normalizeProviderId, normalizeGooglePreviewModelId).
  • openclaw/plugin-sdk/provider-stream - ProviderStreamFamily, buildProviderStreamFamilyHooks(...), composeProviderStreamWrappers(...), plus de gedeelde OpenAI/Codex-wrappers (createOpenAIAttributionHeadersWrapper, createOpenAIFastModeWrapper, createOpenAIServiceTierWrapper, createOpenAIResponsesContextManagementWrapper, createCodexNativeWebSearchWrapper), DeepSeek V4 OpenAI-compatibele wrapper (createDeepSeekV4OpenAICompatibleThinkingWrapper), Anthropic Messages-opruiming van thinking-prefill (createAnthropicThinkingPrefillPayloadWrapper) en gedeelde proxy-/providerwrappers (createOpenRouterWrapper, createToolStreamWrapper, createMinimaxFastModeWrapper).
  • openclaw/plugin-sdk/provider-tools - ProviderToolCompatFamily, buildProviderToolCompatFamilyHooks("gemini") en onderliggende Gemini-schemahelpers (normalizeGeminiToolSchemas, inspectGeminiToolSchemas).
Sommige streamhelpers blijven bewust provider-lokaal. @openclaw/anthropic-provider houdt wrapAnthropicProviderStream, resolveAnthropicBetas, resolveAnthropicFastMode, resolveAnthropicServiceTier en de lagere Anthropic-wrapperbouwers in zijn eigen openbare api.ts / contract-api.ts-seam omdat ze Claude OAuth-bèta-afhandeling en context1m-gating coderen. De xAI-plugin houdt native xAI Responses-vorming op vergelijkbare wijze in zijn eigen wrapStreamFn (/fast-aliassen, standaard tool_stream, opschoning van niet-ondersteunde strikte tools, xAI-specifieke verwijdering van reasoning-payloads).Hetzelfde pakket-rootpatroon ondersteunt ook @openclaw/openai-provider (providerbouwers, default-modelhelpers, realtime-providerbouwers) en @openclaw/openrouter-provider (providerbouwer plus onboarding-/confighelpers).
Voor providers die vóór elke inference-aanroep een tokenuitwisseling nodig hebben:
prepareRuntimeAuth: async (ctx) => {
  const exchanged = await exchangeToken(ctx.apiKey);
  return {
    apiKey: exchanged.token,
    baseUrl: exchanged.baseUrl,
    expiresAt: exchanged.expiresAt,
  };
},
OpenClaw roept hooks in deze volgorde aan. De meeste providers gebruiken er slechts 2-3: Provider-velden die alleen nog voor compatibiliteit bestaan en die OpenClaw niet meer aanroept, zoals ProviderPlugin.capabilities en suppressBuiltInModel, worden hier niet vermeld.
#HookWanneer te gebruiken
1catalogModelcatalogus of standaardwaarden voor base-URL
2applyConfigDefaultsGlobale standaardwaarden in eigendom van de provider tijdens configmaterialisatie
3normalizeModelIdOpschoning van legacy-/preview-model-id-aliassen vóór lookup
4normalizeTransportOpschoning van providerfamilie-api / baseUrl vóór generieke modelassemblage
5normalizeConfigmodels.providers.<id>-config normaliseren
6applyNativeStreamingUsageCompatNative streaming-usage-compat-herschrijvingen voor configproviders
7resolveConfigApiKeyAuth-resolutie van env-markers in eigendom van de provider
8resolveSyntheticAuthLokale/zelfgehoste of config-ondersteunde synthetische auth
9shouldDeferSyntheticProfileAuthSynthetische opgeslagen-profiel-placeholders lager prioriteren achter env-/config-auth
10resolveDynamicModelWillekeurige upstream-model-id’s accepteren
11prepareDynamicModelAsynchrone metadatafetch vóór resolutie
12normalizeResolvedModelTransportherschrijvingen vóór de runner
13contributeResolvedModelCompatCompatflags voor vendormodellen achter een ander compatibel transport
14normalizeToolSchemasOpschoning van toolschema’s in eigendom van de provider vóór registratie
15inspectToolSchemasToolschema-diagnostiek in eigendom van de provider
16resolveReasoningOutputModeContract voor getagde versus native reasoning-uitvoer
17prepareExtraParamsStandaard requestparameters
18createStreamFnVolledig aangepast StreamFn-transport
19wrapStreamFnAangepaste header-/bodywrappers op het normale streampad
20resolveTransportTurnStateNative headers/metadata per turn
21resolveWebSocketSessionPolicyNative WS-sessieheaders/-cooldown
22formatApiKeyAangepaste runtime-tokenvorm
23refreshOAuthAangepaste OAuth-refresh
24buildAuthDoctorHintRichtlijnen voor auth-herstel
25matchesContextOverflowErrorOverflow-detectie in eigendom van de provider
26classifyFailoverReasonClassificatie van rate-limit/overbelasting in eigendom van de provider
27isCacheTtlEligibleTTL-gating voor promptcache
28buildMissingAuthMessageAangepaste hint voor ontbrekende auth
29augmentModelCatalogSynthetische forward-compat-rijen
30resolveThinkingProfileModelspecifieke /think-optieset
31isBinaryThinkingCompatibiliteit voor binair thinking aan/uit
32supportsXHighThinkingCompatibiliteit voor xhigh-reasoningondersteuning
33resolveDefaultThinkingLevelCompatibiliteit voor standaard /think-beleid
34isModernModelRefLive-/smoke-modelmatching
35prepareRuntimeAuthTokenuitwisseling vóór inference
36resolveUsageAuthAangepaste parsing van gebruikscredentials
37fetchUsageSnapshotAangepast gebruiksendpoint
38createEmbeddingProviderEmbeddingadapter in eigendom van de provider voor geheugen/zoeken
39buildReplayPolicyAangepast transcript-replay-/Compaction-beleid
40sanitizeReplayHistoryProvider-specifieke replayherschrijvingen na generieke opschoning
41validateReplayTurnsStrikte replay-turn-validatie vóór de embedded runner
42onModelSelectedCallback na selectie (bijv. telemetry)
Runtime-fallbacknotities:
  • normalizeConfig controleert eerst de gematchte provider en daarna andere hook-capabele providerplugins totdat er één de config daadwerkelijk wijzigt. Als geen providerhook een ondersteunde Google-familie-configentry herschrijft, wordt de meegeleverde Google-confignormalizer nog steeds toegepast.
  • resolveConfigApiKey gebruikt de providerhook wanneer die beschikbaar is. Het meegeleverde amazon-bedrock-pad heeft hier ook een ingebouwde AWS env-marker-resolver, hoewel Bedrock-runtime-auth zelf nog steeds de standaardketen van de AWS SDK gebruikt.
  • resolveSystemPromptContribution laat een provider cachebewuste system-prompt-richtlijnen injecteren voor een modelfamilie. Geef hieraan de voorkeur boven before_prompt_build wanneer het gedrag bij één provider-/modelfamilie hoort en de stabiele/dynamische cachesplitsing moet behouden.
Zie Internals: Provider Runtime Hooks voor gedetailleerde beschrijvingen en praktijkvoorbeelden.
5

Extra capabilities toevoegen (optioneel)

Stap 5: Extra capabilities toevoegen

Een provider-Plugin kan spraak, realtime transcriptie, realtime spraak, mediabegrip, beeldgeneratie, videogeneratie, web-fetch en webzoekopdrachten naast tekstinferentie registreren. OpenClaw classificeert dit als een hybrid-capability-Plugin - het aanbevolen patroon voor bedrijfs-Plugins (een Plugin per leverancier). Zie Internals: Eigendom van capabilities.Registreer elke capability binnen register(api) naast je bestaande api.registerProvider(...)-aanroep. Kies alleen de tabbladen die je nodig hebt:
import {
  assertOkOrThrowProviderError,
  postJsonRequest,
} from "openclaw/plugin-sdk/provider-http";

api.registerSpeechProvider({
  id: "acme-ai",
  label: "Acme Speech",
  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();
    }
  },
});
Gebruik assertOkOrThrowProviderError(...) voor HTTP-fouten van providers, zodat Plugins gedeelde begrensde reads van foutbody’s, JSON-foutparsing en request-id-achtervoegsels gebruiken.
6

Test

Stap 6: Test

src/provider.test.ts
import { describe, it, expect } from "vitest";
// Export your provider config object from index.ts or a dedicated file
import { 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();
  });
});

Publiceren naar ClawHub

Provider-Plugins publiceren op dezelfde manier als elke andere externe code-Plugin:
clawhub package publish your-org/your-plugin --dry-run
clawhub package publish your-org/your-plugin
Gebruik hier niet de verouderde alias voor alleen skills; Plugin-pakketten moeten clawhub package publish gebruiken.

Bestandsstructuur

<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)

Referentie voor catalogusvolgorde

catalog.order bepaalt wanneer je catalogus samenvoegt ten opzichte van ingebouwde providers:
VolgordeWanneerUsecase
simpleEerste passGewone API-key-providers
profileNa simpleProviders afgeschermd via auth profiles
pairedNa profileMeerdere gerelateerde items synthetiseren
lateLaatste passBestaande providers overschrijven (wint bij botsing)

Volgende stappen

Gerelateerd