Examples

AI provider settings (BYOK)

Pattern for a bring-your-own-key deployment: your app hosts an admin page where each tenant picks a provider and saves a key; the editor's aiToolkitResolver calls your backend, which reads the saved key from your secrets store and talks to the provider.

Demo disclaimer. This page stores the configured provider, endpoint, and model in localStorage for convenience. Never put a real API key in browser storage in production - it's a credential leak. In a real deployment, your admin form posts the key to your server and stores it in a secrets manager (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault, or an encrypted-at-rest DB column). The key you enter here is kept only in memory for this page's lifetime.
Production flow

The key never touches browser JavaScript.

1
Admin configures
Tenant admin picks a provider in your app's settings page and pastes a key. Your form POSTs it to your server over HTTPS.
2
Server stores
Your backend writes the key into your secrets manager, scoped to the tenant. The key is never returned to the browser again.
3
Editor asks
The editor's aiToolkitResolver POSTs a request to your /api/ai endpoint. Only the tenant's session cookie identifies the caller.
4
Backend relays
Your endpoint looks up the tenant's key from the secrets manager, calls the provider with it, and returns the result to the editor.

Tenant-configured AI editor

When the provider is set to Built-in demo resolver, Ask AI runs locally with no network call.

Switch to OpenAI / Anthropic / Azure / Custom and Ask AI will route through your endpoint, which in turn uses the tenant's saved key. In this demo, the request is simulated and the synthesized response is returned so you can see the flow end-to-end.

  • Try the Proofread quick action
  • Open Ask AI from the toolbar
  • Watch the Resolver log on the left to see the request shape

Server-side sketch

Example Node/Express handler for /api/ai. Adapt to your stack - the shape is the same in ASP.NET, Go, or Python.

// POST /api/ai  {mode, text, language?}
app.post("/api/ai", requireTenantSession, async (req, res) => {
    const { mode, text, language } = req.body;

    // Look up this tenant's provider + key from YOUR secrets store.
    const cfg = await secrets.getTenantAIConfig(req.tenantId);
    if (!cfg) return res.status(400).json({ error: "Tenant has no AI provider configured." });

    // Call the provider with the tenant's key. Never echo the key back.
    const reply = await providers[cfg.provider].run({
        model: cfg.model, apiKey: cfg.apiKey,
        mode, text, language
    });

    // Return only what the editor needs.
    res.json({ result: reply.text, reason: reply.explanation });
});

Demo code

<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css" />
        <link rel="stylesheet" href="/richtexteditor/plugins/aitoolkit.css" />
        <script type="text/javascript" src="/richtexteditor/rte-config.js"></script>
        <script type="text/javascript" src="/richtexteditor/rte.js"></script>
        <script type="text/javascript" src="/richtexteditor/plugins/all_plugins.js"></script>
        <script type="text/javascript" src="/richtexteditor/plugins/aitoolkit.js"></script>

        <div class="mt-8 grid gap-6 xl:grid-cols-[22rem,minmax(0,1fr)]">
            <!-- Settings column (admin-style) -->
            <aside class="rounded-[1.3rem] border border-slate-200 bg-white p-5 shadow-sm h-fit">
                <div class="flex items-center justify-between mb-4">
                    <div>
                        <div class="text-xs font-bold uppercase tracking-[0.14em] text-sky-700">AI provider</div>
                        <h3 class="mt-1 text-base font-extrabold tracking-tight text-slate-950">Tenant settings</h3>
                    </div>
                    <span class="byok-active-chip" id="byok-active-chip">Demo resolver</span>
                </div>

                <div class="byok-form-grid">
                    <div>
                        <label class="byok-label" for="byok-provider">Provider</label>
                        <select class="byok-select" id="byok-provider">
                            <option value="demo">Built-in demo resolver (no key)</option>
                            <option value="openai">OpenAI</option>
                            <option value="anthropic">Anthropic</option>
                            <option value="azure">Azure OpenAI</option>
                            <option value="custom">Custom endpoint</option>
                        </select>
                        <div class="byok-field-hint">Switch back to demo anytime to run without a real call.</div>
                    </div>

                    <div>
                        <label class="byok-label" for="byok-model">Model</label>
                        <input class="byok-input" id="byok-model" type="text" placeholder="gpt-4o-mini" />
                        <div class="byok-field-hint">Example: <code>gpt-4o-mini</code>, <code>claude-3-5-sonnet</code>, or your deployment name.</div>
                    </div>

                    <div>
                        <label class="byok-label" for="byok-endpoint">Endpoint</label>
                        <input class="byok-input" id="byok-endpoint" type="url" placeholder="/api/ai" />
                        <div class="byok-field-hint">Your backend URL - <strong>not</strong> the provider URL. Your server relays to the provider.</div>
                    </div>

                    <div>
                        <label class="byok-label" for="byok-key">API key</label>
                        <input class="byok-input byok-input--key" id="byok-key" type="password" placeholder="sk-..." autocomplete="off" />
                        <div class="byok-field-hint">In the real flow this POSTs to your server only. Here it stays in memory.</div>
                    </div>

                    <div class="byok-row">
                        <button class="byok-btn byok-btn--primary" type="button" onclick="byok.save();">Save</button>
                        <button class="byok-btn" type="button" onclick="byok.test();">Test connection</button>
                        <button class="byok-btn" type="button" onclick="byok.clear();">Clear</button>
                    </div>

                    <div>
                        <span class="byok-status byok-status--idle" id="byok-status">
                            <span class="byok-status__dot"></span>
                            <span id="byok-status-text">Idle</span>
                        </span>
                    </div>
                </div>

                <div class="mt-5 border-t border-slate-200 pt-4">
                    <div class="text-xs font-bold uppercase tracking-[0.14em] text-slate-500 mb-2">Resolver log</div>
                    <div class="byok-log" id="byok-log"><span class="log-muted">Waiting for first request...</span></div>
                </div>
            </aside>

            <!-- Editor column -->
            <div>
                <div id="div_ai_editor">
                    <h2>Tenant-configured AI editor</h2>
                    <p>When the provider is set to <strong>Built-in demo resolver</strong>, Ask AI runs locally with no network call.</p>
                    <p>Switch to OpenAI / Anthropic / Azure / Custom and Ask AI will route through <strong>your endpoint</strong>, which in turn uses the tenant's saved key. In this demo, the request is simulated and the synthesized response is returned so you can see the flow end-to-end.</p>
                    <ul>
                        <li>Try the <strong>Proofread</strong> quick action</li>
                        <li>Open <strong>Ask AI</strong> from the toolbar</li>
                        <li>Watch the Resolver log on the left to see the request shape</li>
                    </ul>
                </div>

                <div class="mt-4 flex flex-wrap gap-3">
                    <button class="btn btn-primary" onclick="byok.runDialog();">Open Ask AI</button>
                    <button class="btn btn-outline-primary" onclick="byok.runQuick('proofread');">Proofread document</button>
                    <button class="btn btn-outline-primary" onclick="byok.runQuick('summarize');">Summarize</button>
                    <button class="btn btn-outline-primary" onclick="byok.runQuick('rewrite');">Rewrite</button>
                </div>
            </div>
        </div>

        <script>
            //////////////////////////////////////////////////////////////////////
            // BYOK demo - tenant settings + routed resolver
            //
            // In a real app:
            //   - saveSettings() would POST to your server
            //   - the server would write to your secrets manager
            //   - callBackend() would call your /api/ai endpoint with cookies
            //   - your /api/ai would read the tenant's saved key and call the provider
            //
            // Here, we keep settings in localStorage (except the key, which we keep
            // only in memory) so you can exercise the flow without a backend.
            //////////////////////////////////////////////////////////////////////

            var BYOK_STORAGE = "RTE.Demo.BYOK.tenantSettings";
            var runtimeKey = ""; // in-memory only, never persisted

            function readSettings() {
                try {
                    var raw = localStorage.getItem(BYOK_STORAGE);
                    if (!raw) return { provider: "demo", model: "", endpoint: "" };
                    return JSON.parse(raw);
                } catch (e) {
                    return { provider: "demo", model: "", endpoint: "" };
                }
            }

            function writeSettings(s) {
                var safe = { provider: s.provider, model: s.model, endpoint: s.endpoint };
                try { localStorage.setItem(BYOK_STORAGE, JSON.stringify(safe)); } catch (e) {}
            }

            // --- status + log helpers ---
            var statusEl = document.getElementById("byok-status");
            var statusTextEl = document.getElementById("byok-status-text");
            var logEl = document.getElementById("byok-log");
            var chipEl = document.getElementById("byok-active-chip");

            function setStatus(kind, text) {
                statusEl.className = "byok-status byok-status--" + kind;
                statusTextEl.textContent = text;
            }

            function log(kind, msg) {
                if (logEl.querySelector(".log-muted")) logEl.innerHTML = "";
                var line = document.createElement("div");
                line.className = "log-" + kind;
                line.textContent = "[" + new Date().toLocaleTimeString() + "] " + msg;
                logEl.appendChild(line);
                logEl.scrollTop = logEl.scrollHeight;
            }

            function refreshChip() {
                var s = readSettings();
                var label = {
                    demo: "Demo resolver",
                    openai: "OpenAI",
                    anthropic: "Anthropic",
                    azure: "Azure OpenAI",
                    custom: "Custom endpoint"
                }[s.provider] || "Demo resolver";
                chipEl.textContent = label;
            }

            // --- hydrate the form ---
            var providerEl = document.getElementById("byok-provider");
            var modelEl = document.getElementById("byok-model");
            var endpointEl = document.getElementById("byok-endpoint");
            var keyEl = document.getElementById("byok-key");

            (function hydrate() {
                var s = readSettings();
                providerEl.value = s.provider || "demo";
                modelEl.value = s.model || "";
                endpointEl.value = s.endpoint || "";
                refreshChip();
            })();

            // --- simulated backend call ---
            // Returns a Promise that resolves to { result, reason }.
            // In production this would be a real fetch() to your /api/ai endpoint.
            function callBackend(request, settings) {
                return new Promise(function (resolve, reject) {
                    log("req", "POST " + (settings.endpoint || "/api/ai") +
                        "  {provider:" + settings.provider + ", model:" + (settings.model || "-") +
                        ", mode:" + request.mode + ", len:" + (request.source || "").length + "}");
                    // Simulate latency.
                    setTimeout(function () {
                        // Use the plugin's built-in demo result generator so we return
                        // something plausible per mode without a real model call.
                        var synth = aiEditor.aiToolkit.buildDemoResult(
                            request.mode, request.source || "", { language: request.language }
                        );
                        if (!synth) { reject(new Error("empty source")); return; }
                        var reason = "Simulated response from " + settings.provider.toUpperCase() +
                                     (settings.model ? " / " + settings.model : "") +
                                     " via " + (settings.endpoint || "/api/ai") + ".";
                        log("res", "200 OK  len=" + synth.length);
                        resolve({ result: synth, reason: reason });
                    }, 420);
                });
            }

            // --- editor + resolver wired to the tenant settings ---
            var aiEditor = new RichTextEditor("#div_ai_editor", {
                toolbar: "full",
                height: "520px",
                showFloatTextToolBar: true,
                showFloatParagraph: true,
                aiToolkitPersistenceKey: "byok-demo",
                aiToolkitResolver: function (request) {
                    var s = readSettings();

                    // Provider "demo" => let the built-in resolver handle it with no network.
                    if (s.provider === "demo") {
                        log("muted", "provider=demo -> no network call (using built-in resolver)");
                        return null; // falling back to default demo resolver
                    }

                    // Endpoint guard - needed for non-demo providers
                    if (!s.endpoint) {
                        log("err", "No endpoint configured - set one in the settings panel.");
                        setStatus("err", "No endpoint");
                        return { result: "", reason: "No /api/ai endpoint configured for this tenant." };
                    }

                    setStatus("ok", "Calling " + s.provider);

                    return callBackend(request, s).then(function (res) {
                        setStatus("ok", "Response received");
                        return {
                            result: res.result,
                            reason: res.reason,
                            operations: [{
                                type: "preview-suggestion",
                                text: res.result,
                                reason: res.reason
                            }]
                        };
                    }, function (err) {
                        log("err", "request failed: " + err.message);
                        setStatus("err", "Request failed");
                        throw err;
                    });
                }
            });

            // --- public API for the buttons ---
            var byok = {
                save: function () {
                    var s = {
                        provider: providerEl.value,
                        model: modelEl.value.trim(),
                        endpoint: endpointEl.value.trim()
                    };
                    writeSettings(s);
                    runtimeKey = keyEl.value; // kept in memory only
                    refreshChip();
                    if (s.provider === "demo") {
                        setStatus("idle", "Demo resolver active");
                        log("muted", "saved: provider=demo (no network)");
                    } else if (!s.endpoint) {
                        setStatus("err", "Endpoint required");
                        log("err", "saved but missing endpoint");
                    } else if (!runtimeKey) {
                        setStatus("err", "Key required");
                        log("err", "saved but no key - real calls would 401");
                    } else {
                        setStatus("ok", "Settings saved");
                        log("muted", "saved: " + s.provider + " / " + (s.model || "-") +
                            " -> " + s.endpoint + " (key length " + runtimeKey.length + ")");
                    }
                },
                test: function () {
                    byok.save();
                    log("req", "test -> " + providerEl.value);
                    setTimeout(function () {
                        if (providerEl.value === "demo") {
                            setStatus("ok", "Demo OK");
                            log("res", "demo resolver ready");
                        } else if (!endpointEl.value.trim()) {
                            setStatus("err", "No endpoint");
                            log("err", "test failed: endpoint is empty");
                        } else {
                            setStatus("ok", "Simulated OK");
                            log("res", "in production this would GET " + endpointEl.value.trim() + "/healthz");
                        }
                    }, 300);
                },
                clear: function () {
                    try { localStorage.removeItem(BYOK_STORAGE); } catch (e) {}
                    runtimeKey = "";
                    providerEl.value = "demo";
                    modelEl.value = "";
                    endpointEl.value = "";
                    keyEl.value = "";
                    refreshChip();
                    setStatus("idle", "Cleared");
                    log("muted", "settings cleared");
                },
                runDialog: function () {
                    aiEditor.aiToolkit.openDialog({ presetMode: "rewrite", autoRun: false });
                },
                runQuick: function (mode) {
                    aiEditor.aiToolkit.runQuickAction(mode);
                }
            };
        </script>