Skip to content

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:

pascal
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.

ProviderUnitNeedsNetworkStreamsActions
stubbuilt innothingnonono
docsearchHelpChatAI.Provider.DocSearchthe documentation indexnonono
claudeHelpChatAI.Provider.ClaudeANTHROPIC_API_KEYyesyesyes
openaiHelpChatAI.Provider.OpenAIOPENAI_API_KEY (not for a local server)yesyesyes
geminiHelpChatAI.Provider.GeminiGEMINI_API_KEY or GOOGLE_API_KEYyesyesyes

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.

pascal
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.

pascal
HelpChatAI.Settings.Provider := 'openai';
HelpChatAI.Settings.OpenAI.Model := 'gpt-6-luna';
pascal
// 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';
SettingDefaultWhat 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.AzureApiVersion2024-10-21The 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.

pascal
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.

SettingDefaultWhat it does
ApiKey(empty)The key. Empty = the environment variable of that provider
Modelits ownWhich model. Empty = the default of that provider, so you follow the library
BaseUrlits ownEmpty = the official endpoint. This is what points a provider somewhere else
MaxTokens1024Length of a reply
GroundingMaxPages3Documentation pages injected in the prompt
GroundingMaxChars2000Characters of text per page (0 = summary only)
SystemPrompt(built-in)Replaces the built-in instruction
MaxToolRounds4Model calls per reply when actions are used
ConnectTimeoutMs / ResponseTimeoutMs15 s / 120 sTimeouts

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:

  1. the ApiKey of that provider's section, if you set it in code;
  2. %APPDATA%\HelpChatAI\helpchatai.json — shared by every HelpChatAI application of that user;
  3. helpchatai.json next to the executable, then %APPDATA%\<YourApp>\helpchatai.json;
  4. the environment variable — ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY or GOOGLE_API_KEY.
json
{
  "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 ​

pascal
HelpChatAI.Settings.Language := hclItalian;   // hclAuto (default), hclEnglish, hclItalian

The 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:

pascal
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.

Released under the Apache License 2.0