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. Neverput 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
<div id="div_ai_editor"> ... </div>
<script>
var aiEditor = new RichTextEditor("#div_ai_editor", {
toolbar: "full",
height: "520px",
showFloatTextToolBar: true,
showFloatParagraph: true,
aiToolkitPersistenceKey: "byok-demo",
aiToolkitResolver: function (request) {
var s = readSettings();
if (s.provider === "demo") return null; // built-in resolver
if (!s.endpoint) return { result: "", reason: "No /api/ai endpoint configured." };
return callBackend(request, s).then(function (res) {
return {
result: res.result,
reason: res.reason,
operations: [{ type: "preview-suggestion", text: res.result, reason: res.reason }]
};
});
}
});
</script>