Providers
A provider is what turns the conversation plus the screen context into a reply. Which one is used is a string in the settings, so switching is a configuration change:
HelpChatAI.Settings.Provider := 'docsearch';Five ship with the library. A provider registers itself when its unit is linked, so HelpChatAI.Provider.DocSearch, HelpChatAI.Provider.Claude, HelpChatAI.Provider.OpenAI or HelpChatAI.Provider.Gemini has to appear in a uses clause somewhere in the application. That holds with runtime packages too: a package runs the initialization of the units its host references, not of every unit it contains.
An unknown name is an error, not a fallback. A typo, or a provider whose unit nobody linked, used to answer with the placeholder of stub, which reads like a real reply and hides the mistake. Now the question comes back as an error bubble naming the provider you asked for, the unit to add and the providers actually available. Only an empty name means stub.
| Provider | Unit | Needs | Network | Streams | Actions |
|---|---|---|---|---|---|
stub | built in | nothing | no | no | no |
docsearch | HelpChatAI.Provider.DocSearch | the documentation index | no | no | no |
claude | HelpChatAI.Provider.Claude | ANTHROPIC_API_KEY | yes | yes | yes |
openai | HelpChatAI.Provider.OpenAI | OPENAI_API_KEY (not for a local server) | yes | yes | yes |
gemini | HelpChatAI.Provider.Gemini | GEMINI_API_KEY or GOOGLE_API_KEY | yes | yes | yes |
The three model providers answer the same way: your documentation is injected into the system prompt as grounding, the reply streams in token by token, and the assistant can call the actions your application registered.
stub
The default. It answers with a localized placeholder, which is exactly what you want while you are wiring the component up: bubble, F1, panel, streaming, styles — everything works without any dependency.
docsearch
Deterministic, offline, free. It searches the documentation index with the question and the screen context, and replies with the title of the best page, its summary, a "read more" link, and a "see also" when a second page is close enough.
Perfect when:
- you cannot send anything outside the machine;
- you do not want to pay per question;
- your manual is good and the user mostly needs to be sent to the right page.
The retrieval is keyword scoring behind an interface (IHelpChatRetriever): tokens minus Italian and English stop words, light stemming, title hits ×6, keyword hits ×3, body hits capped, and idf weighting so that a word appearing in every page does not decide the ranking.
claude
Anthropic's Messages API, streaming, with your documentation injected as grounding: the retriever picks the pages that match the question and the screen, and their relevant passages are put in the system prompt. The model answers from those, cites the page, and says when it does not know — instead of inventing.
HelpChatAI.Settings.Provider := 'claude';
HelpChatAI.Settings.Claude.Model := 'claude-haiku-4-5';Claude.ApiVersion is the only setting of its own; everything else is the common set.
openai
Chat Completions, streaming, with the same grounding and the same tool use. It is the widest of the three, because a dozen other services chose to speak this API: BaseUrl and Model are all that separates them.
HelpChatAI.Settings.Provider := 'openai';
HelpChatAI.Settings.OpenAI.Model := 'gpt-6-luna';// Groq, OpenRouter, Mistral, DeepSeek, Together...
HelpChatAI.Settings.OpenAI.BaseUrl := 'https://api.groq.com/openai/v1';
HelpChatAI.Settings.OpenAI.Model := 'llama-3.3-70b-versatile';
// A model on the user's own machine: no key, nothing leaves the PC
HelpChatAI.Settings.OpenAI.BaseUrl := 'http://localhost:11434/v1'; // Ollama
HelpChatAI.Settings.OpenAI.Model := 'qwen2.5:7b';
// Azure OpenAI: the deployment is what changes the URL and the auth header
HelpChatAI.Settings.OpenAI.BaseUrl := 'https://my-resource.openai.azure.com';
HelpChatAI.Settings.OpenAI.AzureDeployment := 'my-gpt-deployment';| Setting | Default | What it does |
|---|---|---|
OpenAI.Organization | (empty) | OpenAI-Organization header; empty = not sent |
OpenAI.AzureDeployment | (empty) | Not empty = Azure: the deployment goes in the path and the key in the api-key header |
OpenAI.AzureApiVersion | 2024-10-21 | The api-version of Azure OpenAI |
Two details that save an afternoon: a BaseUrl already ending in /v1 — what Ollama and LM Studio publish — is not given a second one, and an endpoint that is not api.openai.com is never reported as missing a key, because a local server legitimately has none.
gemini
Google's streamGenerateContent, streaming, grounding and tool use as the others. The key travels in the x-goog-api-key header, never in the query string where it would end up in somebody's log.
HelpChatAI.Settings.Provider := 'gemini';
HelpChatAI.Settings.Gemini.Model := 'gemini-3.8-flash';It takes the common settings and nothing else.
What every model provider takes
Settings.Claude, Settings.OpenAI and Settings.Gemini are the same shape, because in practice that is all a model needs. Each is read only when Provider names it.
| Setting | Default | What it does |
|---|---|---|
ApiKey | (empty) | The key. Empty = the environment variable of that provider |
Model | its own | Which model. Empty = the default of that provider, so you follow the library |
BaseUrl | its own | Empty = the official endpoint. This is what points a provider somewhere else |
MaxTokens | 1024 | Length of a reply |
GroundingMaxPages | 3 | Documentation pages injected in the prompt |
GroundingMaxChars | 2000 | Characters of text per page (0 = summary only) |
SystemPrompt | (built-in) | Replaces the built-in instruction |
MaxToolRounds | 4 | Model calls per reply when actions are used |
ConnectTimeoutMs / ResponseTimeoutMs | 15 s / 120 s | Timeouts |
Defaults are claude-haiku-4-5, gpt-6-luna and gemini-3.8-flash: the small, cheap models of each family, which for in-app help are the right ones — short answers, read from documents you already wrote.
Where the key goes
Never in your source code, never in a file you ship. The key is read, in order, from:
- the
ApiKeyof that provider's section, if you set it in code; %APPDATA%\HelpChatAI\helpchatai.json— shared by every HelpChatAI application of that user;helpchatai.jsonnext to the executable, then%APPDATA%\<YourApp>\helpchatai.json;- the environment variable —
ANTHROPIC_API_KEY,OPENAI_API_KEY,GEMINI_API_KEYorGOOGLE_API_KEY.
{
"Claude": { "ApiKey": "sk-ant-..." },
"OpenAI": { "ApiKey": "sk-..." },
"Gemini": { "ApiKey": "..." }
}Naming a file in ConfigFileName makes that one the whole configuration, and the list above does not apply.
On a desktop the key is on the user's machine and recoverable by whoever owns it. When that matters — an application installed at customers, or anything mobile — move the engine to a HelpChatAI server: the clients then authenticate with an application key that grants nothing but the right to ask questions.
The language of the assistant
HelpChatAI.Settings.Language := hclItalian; // hclAuto (default), hclEnglish, hclItalianThe language sets the interface strings of the panel and is told to the model, which replies in that language unless the user clearly writes in another one. hclAuto follows the OS locale.
Writing your own provider
A provider is a class registered by name:
type
TMyProvider = class(THelpChatProviderBase)
public
function GetName: string; override;
function SupportsStreaming: Boolean; override;
function Generate(const AHistory: TArray<THelpChatMessage>;
const AContext: THelpChatContext; const AOnToken: THelpChatTokenProc;
const ACancel: IHelpChatCancelToken): string; override;
end;
initialization
THelpChatProviderRegistry.RegisterProvider('mine', TMyProvider);Inside Generate you have Settings (a private snapshot of the request), Retriever (the documentation index) and Actions. Two obligations: call AOnToken as tokens arrive if you say you stream, and check ACancel between steps — a user who stops a reply, or an application that is closing, must not wait for you.
You do not have to build the prompt yourself: BuildSystemPrompt already assembles the instruction, the language rule, the screen context and the documentation excerpts, the same way for everybody. If you stream, THelpChatSseStreamBase (HelpChatAI.Provider.Sse) does the byte-level half — chunks cut into lines, UTF-8 decoded only once a line is whole, one token at a time to the caller — and leaves you one method to write, ProcessLine.
Override ConfigurationIssue when your provider needs something to work, returning one sentence that says what is missing. That is what the design-time check and HelpChatAI.Diagnostics report: they ask the provider rather than knowing about it, so your provider is checked like the built-in ones.