Running in production
Defaults are tuned for a typical SaaS customer. Below are the knobs you’ll touch when traffic shapes change — rate limiting, retry policy, audit logging, dependency reachability probes,IHealthCheck integration, idempotency, prompt-template overrides, and the BYOK admin surface.
Rate limiting AI calls
Both POST /richtextbox/ai and POST /richtextbox/ai/streamshare a per-IP token bucket so a single runaway tab can’t burn through your provider quota in seconds. Defaults: 30 calls per minute per IP.
builder.Services.AddRichTextBox(opts => { opts.AiRateLimit = 30; // calls per window opts.AiRateLimitWindow = TimeSpan.FromMinutes(1); // window length });
Set AiRateLimit = 0 to disable, or swap in a Redis-backed limiter:
builder.Services.AddSingleton<IRichTextBoxAiRateLimiter, RedisAiRateLimiter>(); builder.Services.AddRichTextBox();
HTTP retry on transient provider failures
OpenAI / Anthropic / Azure OpenAI return 429 and 503under normal load. The built-in resolvers retry these (plus 502 / 504 + connection drops) with exponential backoff and full jitter, capped at 30 seconds. The provider’s Retry-After header is honoured.
services.AddRichTextBoxOpenAiResolver(opts => { opts.ApiKey = builder.Configuration["OpenAI:ApiKey"]; opts.MaxRetryAttempts = 2; // 0 to disable opts.RetryBaseDelay = TimeSpan.FromMilliseconds(500); });
Idempotency keys
Every POST /richtextbox/ai, /ai/stream, and /upload respectsIdempotency-Key. Duplicate retries replay the cached body verbatim — no double charging, no orphan files. Streaming responses replay the entire SSE byte stream byte-for-byte.
builder.Services.AddRichTextBox(opts => { opts.IdempotencyTtl = TimeSpan.FromHours(1); opts.IdempotencyPruneInterval = TimeSpan.FromMinutes(5); });
Multi-instance hosts: register a Redis-backed IIdempotencyStore instead of the default in-memory one. Native TTL means you can disable the periodic pruner.
Security audit logging
Every security-relevant rejection emits a structured LogWarning under categoryRichTextBox.Audit with a stable EventId:
| EventId | Name | Fires when |
|---|---|---|
| 8001 | LicenseInvalid | An endpoint refused a request because the .lic file was missing or invalid. |
| 8101 | UploadMagicByteMismatch | Magic bytes don’t match the declared extension. |
| 8102 | UploadRejectedByValidator | An IUploadValidator returned Reject. |
| 8103 | UploadInvalidExtension | Upload rejected for being outside the allow-list. |
| 8104 | UploadOversize | Exceeded MaxUploadBytes. |
| 8201 | AiRequestOversize | AI body exceeded MaxRequestBytes. |
| 8202 | AiRateLimitExceeded | Caller exceeded the per-IP AI rate limit. |
| 8203 | AiResolverException | The AI resolver threw; logged with correlation id. |
| 8204 | AiKeyVaultMiss | BYOK vault returned no key. |
| 8205 | AiKeyVaultHit | BYOK vault returned a key (KeyId logged; secret never). |
| 8206 | AiResponseFilterMatched | An IRichTextBoxAiResponseFilter mutated the response. |
Routing audit events to a separate stream
IRichTextBoxAuditSinklets compliance-regulated workloads (HIPAA / PCI / SOC2) route just audit events to a write-once destination — Splunk HEC, S3 with object-lock, an audit-only DB. Ships with JsonLinesAuditSink for offline / single-instance use:
builder.Services.AddRichTextBox(); builder.Services.AddJsonLinesAuditSink("/var/log/myapp/rtb-audit.jsonl");
Channel + background drainer; RecordAsyncnever blocks. Queue overflow drops events with a DroppedCountmetric. Multiple sinks register naturally — pair JsonLines with a real-time Splunk sink for free.Dependency reachability probes
Optional companion interfaces let any RichTextBox dependency report whether its backing service is reachable. The /richtextbox/health endpoint surfaces the result alongside license status:
| Interface | Built-in implementations | Probe target |
|---|---|---|
IRichTextBoxAiResolverProbe | OpenAI, Azure OpenAI (free); AnthropicResolverWithProbe wrapper (low-token POST) | GET /v1/models / GET /openai/deployments / POST /v1/messages with max_tokens=1 |
IIdempotencyStoreProbe | Custom Redis / SQL store (you implement) | PING / SELECT 1 |
IRichTextBoxUploadStoreProbe | Custom S3 / Azure Blob / Cloudinary store | HEAD bucket / list-objects --max-items 1 |
builder.Services.AddRichTextBox(opts => { opts.AiResolverHealthProbeEnabled = true; opts.AiResolverHealthProbeTimeout = TimeSpan.FromSeconds(3); opts.AiResolverHealthProbeCacheTtl = TimeSpan.FromSeconds(30); opts.IdempotencyStoreHealthProbeEnabled = true; opts.UploadStoreHealthProbeEnabled = true; });
Cache TTL on each probe defends against load-balancer hammering — a balancer hitting /healthevery 5 seconds would otherwise produce 12 probes per minute even when probes cost real money.
Standard IHealthCheck integration
For hosts using Microsoft.Extensions.Diagnostics.HealthChecks, register the adapter and RichTextBox status appears alongside your other checks — no parallel monitoring path:
builder.Services.AddRichTextBox(); builder.Services.AddHealthChecks() .AddRichTextBox(name: "richtextbox", failureStatus: HealthStatus.Unhealthy, tags: new[] { "ready" } ); app.MapHealthChecks("/health");
Returns Healthy when license + all gated probes pass; Unhealthy with a description identifying the failed dependency. HealthCheckResult.Data mirrors the JSON payload of /richtextbox/health.
BYOK — per-tenant API keys
Multi-tenant SaaS hosts charge AI traffic to each tenant’s own provider account viaIAiKeyVault + IAiKeyVaultAdmin. The package ships an in-memory reference implementation; production deployments back onto Azure Key Vault, Redis, or a real SQL store.
See the dedicated BYOK key vault page for the full contract, admin REST endpoints, and reference implementations (Redis, Azure Key Vault, encrypted-on-diskFileBackedAiKeyVault).
Per-call cost ledger
IRichTextBoxAiCostSink emits one AiUsageRecordper AI call — provider, model, mode, tokens (input / output / total), latency, status, KeyId. Forward to your billing system for tenant chargeback:
builder.Services.AddSingleton<IRichTextBoxAiCostSink, MyBillingApiCostSink>();For high-traffic tenants, wrap with the buffering sink so the inner sees one batched flush per interval instead of one HTTP/SQL call per AI request. If your inner sink also implementsIRichTextBoxAiCostBatchSink, the wrapper routes through RecordBatchAsync— one INSERT instead of N.
builder.Services.AddBufferingAiCostSink<MyBillingApiCostSink>( flushInterval: TimeSpan.FromSeconds(30), maxBatchSize: 500);
Output filtering — PII redaction, content blocking
IRichTextBoxAiResponseFilter runs after the resolver but before the response goes to the wire. The reference RegexAiResponseFilter covers ~80% of practical PII redaction:
var emailMask = new Regex(@"[\w.+-]+@[\w-]+\.[\w.-]+"); var ssnMask = new Regex(@"\b\d{3}-\d{2}-\d{4}\b"); builder.Services.AddSingleton<IRichTextBoxAiResponseFilter>( new RegexAiResponseFilter( (emailMask, "[email-redacted]"), (ssnMask, "[ssn-redacted]")));
For streaming responses, the framework buffers tokens server-side when filters are registered (BufferStreamingForFilters = true default) so a credit-card regex spanning multiple deltas actually matches.
Prompt-template overrides
IRichTextBoxPromptTemplateProvider lets you override the per-mode system prompts the built-in resolvers send. Use the PromptTemplateProviderBase decorator base class to override only the method(s) you need:
public sealed class FormalTonePromptProvider : PromptTemplateProviderBase { public override string BuildSystemPrompt(RichTextBoxAiRequest request, string? operatorSuffix) => base.BuildSystemPrompt(request, operatorSuffix) + "\n\nAlways reply in formal British English; avoid contractions."; }
For one-off mode tweaks without a full provider, use the dictionary:
services.AddRichTextBoxOpenAiResolver(opts => { opts.ApiKey = "sk-..."; opts.SystemPromptOverridesByMode["proofread"] = "You are a hyper-pedantic copy-editor for legal documents."; });
Prompt-injection screening
IRichTextBoxAiPolicy is the gate the AI endpoints consult before invoking the resolver. The built-in DefaultPromptInjectionPolicyrejects off-the-shelf jailbreak templates (“ignore previous instructions”, “you are now DAN”, etc.). Layer additional policies for PII redaction or tenant-scoped mode allow-lists.
builder.Services.AddSingleton<IRichTextBoxAiPolicy, DefaultPromptInjectionPolicy>(); builder.Services.AddSingleton<IRichTextBoxAiPolicy, MyTenantPolicy>();
Distributed tracing (OpenTelemetry)
Every AI call, DOCX export, and upload emits a span through theRichTextBox.AspNetCore ActivitySource. Subscribe in your tracer-provider configuration:
builder.Services.AddOpenTelemetry().WithTracing(t => t .AddSource(RichTextBoxDiagnostics.SourceName) // "RichTextBox.AspNetCore" .AddAspNetCoreInstrumentation() .AddOtlpExporter());
Startup-time options validation
RichTextBoxOptionsValidator + AiResolverOptionsValidator<T> run at the first IOptions.Valueresolution — before the host accepts the first request. Bad config (forgotten /, negative byte cap, heartbeat > timeout, missing API key, missing Azure deployment) throws OptionsValidationException at startup so deployments fail loudly.
No wiring required. The validators register automatically via AddRichTextBox().
Companion docs
BYOK key vault·AI providers & streaming·Cloud upload providers·Accessibility