ServiceStack.AI.Chat

Your organization's own OpenRouter running in your .NET App

One plugin turns any ServiceStack App into a private AI gateway - one set of provider accounts and one bill for the whole organization, reaching only the models you've approved, behind the ASP.NET Identity Auth you already have.

Your users
alex Admin
sam Staff
contractor no role
Identity Auth
RequireAuth true
RequiredRole "Staff"
2 allowed · 1 rejected
Approved models
claude-opus-5 gpt-5 gemini-3-pro llama-4 +526 more
centralized provider keys & billing

No second user directory. No SSO integration project. The people who can already sign into your App are the people who can use it - and the ones who can't, can't.

Governed by your App

It's already your users

ChatFeature authenticates against the App it's hosted in. Signing in, roles, locking an account, revoking a key - all of it keeps working exactly as it does everywhere else in your App.

Signing in to the Chat UI with an Identity Auth account
Managing users from the Admin UI

Restrict who gets in

Set RequiredRole and only users holding that role reach the UI or any of its APIs. Everyone else is rejected at the door.

Keys for programmatic clients

With ApiKeysFeature registered, /v1/chat/completions also accepts a Bearer key - so scripts and services keep working alongside browser sign-in.

Multi-user from the start

Everyone gets their own workspace. Chat history, uploads and gallery, agent profiles, projects, skills, themes and PDF templates are all partitioned per user - on top of whatever you publish to the whole organization.

Configure.AI.cs
services.AddPlugin(new ChatFeature {
    RequireAuth  = true,
    RequiredRole = "Staff",       // only Staff can use it
    RoutePrefix  = "/chat",
    EnableProviders = ["openai", "anthropic", "google"],
});

One endpoint, every model

530+ models. 24 providers. Your rules.

Give your organization one OpenAI-compatible endpoint that reaches every major provider - then decide which models it's allowed to reach, and who gets which ones.

Choosing between models grouped by provider
The model selector filtered by capability

Profiles tailored per team

A profile bundles a system prompt, an allowed model list, its tools and a theme. Publish a read-only profile to the whole organization, or let each team shape its own.

Switching between agent profiles
Restricting a profile to an approved model list
A read-only profile published to the organization

Accounted for, and kept

Know your costs, and who spent it

Every completion is recorded against the user who made it - model, provider, input and output tokens, and cost. It's a row in your own database, so you can bill a department, find a runaway agent, or answer "what are we spending on AI" without asking a vendor.

  • Cost broken down by model and by provider
  • Daily totals, token usage and activity over time
  • Stored in your RDBMS via OrmLite - query it however you like
  • Kept - every request stays queryable, not just this month's
Cost analysis showing total spend broken down by model and provider
Token usage over time
Request activity over time

Admins see across every user

  • Searchable user filter - point cost, tokens and activity at one user or all of them
  • Users tab - requests, cost, input/output/total tokens and last active, per user
  • Sort by any column - find the heaviest spender or who's gone quiet
  • Open any transcript - read the conversation behind a request, straight from the activity log
Searchable user selector filtering analytics to one user
Users tab breaking down requests, cost and tokens for every user

API Tools & MCP

Point that intelligence at your own APIs

A model that can only talk is a demo. Let it call the APIs your App already exposes and it can answer questions about your business - then hand the same tools to the AI Assistant your developers already use.

Inside the Chat UI

Opt an API in with [Tool] - or whole groups of them by [Tag] - and the assistant can find it, read its schema and call it.

It runs as the signed-in user, so it can only ever do what that person is already allowed to do.

api_search - find an API by keyword or tag
api_describe - get its JSON Schema
api_call - run it, return the JSON

Three tools rather than one per API - a 270-API surface is about 156K tokens of schema, so only the APIs an agent actually uses ever cost it context.

From any MCP Client

Expose the same tools as an MCP Server at /chat/mcp and external assistants - Claude Code, Cursor, VS Code - can work against your APIs directly.

Clients authenticate with a Bearer API Key and tools run as that key's user, not as the App. Nothing is exposed until you name it.

new ChatFeature {
    ToolsConfig = new() { EnableApiTools = true },
    Mcp = new() {
        ToolGroups = ["api_tools"],
    },
}
A model calling a tool and showing the result inline
Configured MCP servers

Gemini File Search

Answers from your own documents

Upload the documents your organization actually runs on and Gemini answers out of them - grounded in what you uploaded, not in what it read on the internet. No vector database to stand up.

employee handbook contracts runbooks product specs compliance policies support history
Uploading a folder of documents into a file store, organized by category

Drop in a folder, not a pipeline

No embedding job to run, no chunking strategy to tune, no index to operate. Upload a folder and it's searchable - Google hosts the store, you keep the catalogue.

Narrow the question

Ground a conversation on an entire store, on one category, or on a single document - so an answer about last year's policy can't be contaminated by this year's.

Uploaded once, kept honest

Documents are deduplicated by content hash and uploaded by a background worker. A sync reconciles your catalogue against the store and records anything that didn't line up.

Behind the same front door

Stores are reached through the same Identity Auth as everything else, and catalogued in your own RDBMS - so who can ask your knowledge base a question is a role, not a separate product's setting.

Grounding a conversation on an entire document store
Narrowing the search to a single category
Asking a question against one document
Managing document stores
Syncing a store and reviewing each document's state

Set GEMINI_API_KEY and the extension turns itself on; leave it unset and it disables itself. Stores and documents are recorded in the ChatFilestore and ChatDocument tables.

PDF Studio

Design and edit PDF templates with AI.
Render it with your own data, fast.

Many Apps eventually have to produce a real PDF. Design it with AI against a live preview, and get back a data-driven template your App fills with its own data - rendered by typst.

invoices quotes statements packing slips certificates
PDF Studio: file explorer, Typst template, live preview and AI prompt

Rendered by typst

Templates are plain-text typst you can diff and review, compiled by a single fast Rust binary. No headless browser rendering differently on every machine, no Windows-only report designer.

Data-driven, not baked in

The data lives separately from the document. One template renders every invoice you'll ever send - your App supplies the data, so PDFs can be generated on the fly instead of designed one at a time.

Typed end to end

Publishing generates C# classes for the template's data, so rendering a PDF in an API - or attaching one to an email - is type-checked at compile time rather than hand-built JSON and a hope that the keys match.

A shared lib for common elements

Letterhead, footers and house styling live in a shared lib template every document can pull from - so rebranding is one edit, not one per document.

A template's data edited through a generated form
The same data edited as JSON
C# classes generated from a published template
The shared lib template holding letterhead and footer elements

Edit with AI

Rebuild an existing document as a typst template, or start from nothing - then keep refining in place.

  • Recreate what you already send - attach a screenshot or an existing PDF and get a template back
  • Or start from scratch - describe the document you want in plain English
  • Keep refining - each edit is another prompt against the live preview, not a rewrite
  • Bring a better model - it's your gateway, so point it at the strongest one you have
Attaching a screenshot of an existing document to rebuild it as a template
The template before an AI edit
The same template after the AI applied the requested change
Page setup for size, orientation and margins

Authoring is ChatFeature; rendering is PdfFeature, and it stands alone - deploy the renderer with no API key, no model and no designer.

From template to inbox

The generated model is the whole integration. Populate it from your own tables, then either hand the bytes back as a download or attach them to an email - same template, same model, two lines apart.

Serve it as a download

InvoiceServices.cs
public async Task<object> Get(DownloadInvoice request)
{
    var order = await Db.LoadSingleByIdAsync<Order>(request.Id);
    var invoice = order.MapToInvoice();

    return await pdf.PdfResultAsync(invoice,
        $"Invoice-{order.InvoiceNo}.pdf");
}
Content-Type: application/pdf
Content-Disposition: attachment; filename="Invoice-INV-2026-042.pdf"

Attach it to an email

SendInvoiceEmailCommand.cs
[Worker("smtp")]
public class SendInvoiceEmailCommand(IPdfRenderer pdf,
    SmtpConfig config, IDbConnectionFactory dbFactory)
    : AsyncCommand<SendInvoiceEmail>
{
    protected override async Task RunAsync(
        SendInvoiceEmail request, CancellationToken token)
    {
        using var db = await dbFactory.OpenAsync(token: token);
        var order = await db.LoadSingleByIdAsync<Order>(
            request.OrderId, token: token);

        // same model, rendered to bytes instead
        var pdfBytes = await pdf.RenderPdfAsync(
            order.MapToInvoice(), token);

        using var msg = new MailMessage(config.FromEmail, request.To) {
            Subject = request.Subject,
            Body    = request.BodyText,
        };
        msg.Attachments.Add(new Attachment(
            new MemoryStream(pdfBytes), "invoice.pdf", MimeTypes.Pdf));

        using var client = new SmtpClient(config.Host, config.Port);
        await client.SendMailAsync(msg, token);
    }
}
Queue it like any other Command
jobs.EnqueueCommand<SendInvoiceEmailCommand>(new() {
    To      = order.Email,
    Subject = $"Your invoice {order.InvoiceNo}",
    OrderId = order.Id,
});

Queue the id, not the bytes

Job arguments are persisted, so passing the rendered PDF into the queue writes it into your jobs table. Pass the OrderId and render inside the worker - SMTP is slow and flaky, which is exactly why it belongs in a background job that can retry.

Beautiful themes

Eight handcrafted themes ship with it, and a profile can carry its own - so a team's workspace looks like theirs. Pick one to see it full size.

Nord theme
Nord
Matrix theme
Matrix
Blue Smoke theme
Blue Smoke
Dark theme
Dark
Light Slate theme
Light Slate
Soft Pink theme
Soft Pink
Light Sky theme
Light Sky
Light theme
Light

Typed APIs

The same gateway, from your own code

The providers, keys and approved models you configured for the Chat UI are the same ones your App calls - in-process from C#, or over the OpenAI-compatible endpoint from any language.

In-process, no HTTP

Inject IChatClient and your code runs through the very same pipeline the Chat UI does:

  • Provider selection, retry and failover
  • The tool-execution loop
  • Usage and cost accounting, in the same tables
  • One place to rotate a key or approve a model
SupportServices.cs
public class SupportServices(IChatClient chat) : Service
{
    public async Task<object> Any(SummarizeTicket request)
    {
        var api = await chat.ChatAsync(new() {
            Model = "claude-opus-5",
            Messages = [
                new() { Role = "user", Content = [
                    new AiTextContent {
                        Type = "text",
                        Text = $"Summarize: {request.Body}",
                    }
                ]}
            ]
        });

        return new SummarizeTicketResponse {
            Summary = api.Choices[0].Message.Content
        };
    }
}

Or over the wire, from anywhere

The gateway speaks OpenAI's wire format at /v1/chat/completions, so existing clients just work. And because the request is a ServiceStack API, Add ServiceStack Reference generates typed DTOs for it - no hand-written JSON, in 15 languages.

Same Bearer API Key as the Chat UI, so a script gets the same approved models and lands in the same accounting as everyone else.

.NET
Web
Mobile
Systems
Program.cs

Loading examples…

And the rest of it

The same Chat UX as llms.py, re-implemented as a ServiceStack plugin on Identity Auth, OrmLite and your App_Data folder.

Text, image & audio

Chat, generate images, synthesize and transcribe speech - from whichever providers you've enabled.

Agents & system prompts

Purpose-built agent profiles and a library of system prompts, editable per user.

Skills

Anthropic-style skill packages, installable per user or shared across the organization.

Gemini File Search

Build document stores and ground answers on them - RAG without standing up a vector database.

Projects

Per-user working folders that the filesystem tools are sandboxed to while a project is active.

Code execution, opt-in

Python, TypeScript, JavaScript and C# run in a temp directory with a stripped environment - off by default.

Media gallery

Everything uploaded or generated, deduplicated by content hash and browsable per user.

OpenAI-compatible API

Existing clients point at /v1/chat/completions and keep working.

Your database

Threads and accounting persist through OrmLite to any supported RDBMS. Schema updates apply on startup.

Add it to an App you already run

Add the package, register the plugin, set the provider API keys you want to use, and browse to /chat.