Chat and prompting
Every request TTMSMCPCloudAI makes is built from the same three ingredients:
which service to talk to, what to say, and how the model should generate its
answer. The component keeps those separated. Service and APIKeys decide the
destination, SystemRole / AssistantRole / Context supply the prompt, and
Settings carries the per-service model names plus the generation parameters.
Because every provider reads its own model property, you can configure all of
them once and switch providers by changing a single enum value — which is what
makes a service picker practical in a shipped application. This guide covers
provider selection and key storage, the three prompt roles, the generation
settings including the newer UseTemperature and Claude CacheControl
options, local and aggregated endpoints, and how to read the answer and account
for what it cost.
Selecting a service and a model
Start here because everything else depends on it: the model names, the generation parameters a service accepts, and even whether file or function support is available differ per provider.
procedure TForm1.UseProvider(AService: TTMSMCPCloudAIService);
begin
AI.Service := AService;
{ Each service reads its own model property, so all of them can be
configured up front and the Service property alone decides which is used. }
AI.Settings.OpenAIModel := 'gpt-5.2';
AI.Settings.ClaudeModel := 'claude-sonnet-4-5';
AI.Settings.GeminiModel := 'gemini-2.5-pro';
AI.Settings.GrokModel := 'grok-4';
AI.Settings.MistralModel := 'mistral-large-latest';
AI.Settings.DeepSeekModel := 'deepseek-chat';
AI.Settings.PerplexityModel := 'sonar-pro';
AI.Settings.OpenRouterModel := 'openai/gpt-5.2';
{ Data-residency variant of the OpenAI endpoint. }
AI.Settings.OpenAIServers := oaEU;
end;
The TTMSMCPCloudAIService value and its
matching key and model property line up like this:
Service |
Key | Model property |
|---|---|---|
aiOpenAI |
APIKeys.OpenAI |
Settings.OpenAIModel |
aiClaude |
APIKeys.Claude |
Settings.ClaudeModel |
aiGemini |
APIKeys.Gemini |
Settings.GeminiModel |
aiGrok |
APIKeys.Grok |
Settings.GrokModel |
aiMistral |
APIKeys.Mistral |
Settings.MistralModel |
aiDeepSeek |
APIKeys.DeepSeek |
Settings.DeepSeekModel |
aiPerplexity |
APIKeys.Perplexity |
Settings.PerplexityModel |
aiOpenRouter |
APIKeys.OpenRouter |
Settings.OpenRouterModel |
aiOllama |
— | Settings.OllamaModel |
aiLlamaCpp |
— | Settings.LlamaCppModel |
Settings.OpenAIServers switches the OpenAI endpoint between the default
(oaDefault) and the EU-resident one (oaEU) — see
TTMSMCPCloudOpenAIServers.
Keys are stored on TTMSMCPCloudAIAPIKeys,
which encrypts them with SaveToFile(AFileName, Password) and reads them back
with LoadFromFile. Keep them out of source and out of version control.
Building the prompt: three roles
A prompt is rarely just a question. Splitting it lets you set policy once and vary only the user's turn.
SystemRole— standing instructions, sent with every request. Tone, format rules, domain constraints, a schema description.AssistantRole— a seeded assistant turn. Useful to demonstrate the reply shape you want without asking for it in prose.Context— the user turn: the actual question or material.
All three are TStrings, so multi-line content and LoadFromFile work
directly.
procedure TForm1.AskAboutInvoice(const AQuestion: string);
begin
{ Standing instructions - sent with every request, not part of the question. }
AI.SystemRole.Clear;
AI.SystemRole.Add('You are an accounting assistant for a Delphi application.');
AI.SystemRole.Add('Answer with plain text. Never invent invoice numbers.');
{ An assistant turn seeded by the application steers the reply format
without the user having to ask for it. }
AI.AssistantRole.Clear;
AI.AssistantRole.Add('I reply with one short paragraph followed by a bullet list.');
{ The user turn. }
AI.Context.Clear;
AI.Context.Add(AQuestion);
AI.Execute;
end;
Generation settings
These control how the model answers rather than what it is asked. Getting them wrong is the most common cause of a request that is rejected outright.
procedure TForm1.ConfigureGeneration;
begin
{ Deterministic-leaning output for a data extraction task. }
AI.Settings.Temperature := 0.2;
{ Newer reasoning models reject a temperature parameter. Setting
UseTemperature to False omits it from the request body altogether,
instead of sending a value the service will refuse. }
AI.Settings.UseTemperature := False;
{ 0 leaves the limit to the service. }
AI.Settings.MaxTokens := 2048;
{ Thinking budget on models that expose one. aiIgnore leaves the request
untouched; aiOff asks the model not to spend reasoning tokens. }
AI.Settings.Reasoning := aiHigh;
{ Let the service ground its answer in live search results. }
AI.Settings.WebSearch := True;
{ Anything the component does not surface as a property can be merged into
the request body as raw JSON. }
AI.Settings.CustomOptions := '"top_p": 0.9';
end;
| Setting | Effect |
|---|---|
Temperature |
Randomness. Lower is more deterministic. |
UseTemperature |
When False, the temperature parameter is omitted from the request body entirely. Default True. |
MaxTokens |
Upper bound on the answer length. 0 leaves it to the service. |
Reasoning |
Thinking effort on models that expose one: aiLow, aiMedium, aiHigh, aiOff, or aiIgnore to send nothing. |
WebSearch |
Lets the service ground its answer in live search results, where supported. |
ParallelToolExecution |
Allows several tool calls in one turn — see Function calling. |
CustomOptions |
Raw JSON fragment merged into the request body, for anything not surfaced as a property. |
UseTemperature exists because several newer reasoning models reject a
temperature parameter rather than ignoring it. Setting Temperature := 0 is not
the same thing — the parameter is still sent. Set UseTemperature := False and
the component leaves it out.
Reasoning defaults to aiIgnore, which sends no reasoning field at all; that
is the safe value for a model or service that does not understand one. See
TTMSMCPCloudAIReasoning.
Caching a long Claude prompt
A large system role — a style guide, a schema, a policy document — is re-sent and re-billed on every turn of a conversation. Claude can cache such a block and reuse it.
procedure TForm1.ConfigureClaudeWithCachedInstructions;
begin
AI.Service := aiClaude;
AI.Settings.ClaudeModel := 'claude-sonnet-4-5';
{ A large, stable system prompt - a style guide, a schema description, a
policy document - is re-sent with every single turn of a conversation. }
AI.SystemRole.LoadFromFile(InstructionsFileName);
{ ccEphemeral adds Claude's cache_control marker to the request, so the
service can reuse its own cached copy of that block instead of
reprocessing it. ccNone (the default) sends no marker. }
AI.Settings.CacheControl := ccEphemeral;
AI.Context.Text := memoPrompt.Lines.Text;
AI.Execute;
end;
Settings.CacheControl takes a
TTMSMCPCloudAICacheControl value:
ccNone (the default) sends no cache marker, and ccEphemeral adds Claude's
cache_control marker so the service may serve the block from its own cache.
The setting only affects requests made with Service = aiClaude; other services
ignore it. It pays off when the cached block is large and stable and the same
conversation continues over several turns — it does nothing for a single
one-shot request with a short system role.
Local servers and aggregators
Not every deployment can send data to a hosted service, and not every project wants one key per vendor.
procedure TForm1.UseLocalOllama;
begin
AI.Service := aiOllama;
AI.Settings.OllamaHost := 'localhost';
AI.Settings.OllamaPort := 11434;
AI.Settings.OllamaPath := '/api/chat';
AI.Settings.OllamaModel := 'llama3.2';
{ A local server needs no API key. }
end;
procedure TForm1.UseLocalLlamaCpp;
begin
AI.Service := aiLlamaCpp;
AI.Settings.LlamaCppHost := 'localhost';
AI.Settings.LlamaCppPort := 8080;
AI.Settings.LlamaCppPath := '/v1/chat/completions';
{ llama.cpp ships without a model. Reference a downloaded GGUF model by its
repository name, for example Qwen/Qwen3-4B-GGUF. }
AI.Settings.LlamaCppModel := 'Qwen/Qwen3-4B-GGUF';
end;
procedure TForm1.UseOpenRouter;
begin
AI.Service := aiOpenRouter;
{ One key, many upstream models - the model string selects the provider. }
AI.Settings.OpenRouterModel := 'anthropic/claude-sonnet-4.5';
end;
aiOllama and aiLlamaCpp talk to a server you run, addressed by
OllamaHost / OllamaPort / OllamaPath and LlamaCppHost / LlamaCppPort /
LlamaCppPath. Neither uses an API key.
aiOpenRouter is an aggregator: one key, and Settings.OpenRouterModel names
the upstream model in vendor/model form. It is the quickest way to reach a
model whose vendor you have no direct account with.
Setting up a llama.cpp server
llama.cpp is the one local option that needs preparing before aiLlamaCpp can
answer anything, because it is installed and stocked separately from the
component. Three steps, once per machine:
- Install the server. Take the precompiled binaries for your platform from the llama.cpp releases page, or build it yourself by following the build guide.
- Download a model. The installation comes with none — a llama.cpp server with no model answers every request with an error. Pick a GGUF model from Hugging Face and download it.
- Name the model in
LlamaCppModel. Use its repository form,username/model. For the Qwen3 4B GGUF model, that isQwen/Qwen3-4B-GGUF.
Execute raises at the call site when Service = aiLlamaCpp and
Settings.LlamaCppModel is empty, so an unnamed model surfaces immediately
rather than as a service error. The default endpoint is localhost:8080 with
path /v1/chat/completions; change LlamaCppHost, LlamaCppPort, and
LlamaCppPath when the server runs elsewhere.
Ollama needs no equivalent step beyond installing it and pulling a model with
ollama pull; it defaults to localhost:11434 with path /api/chat.
Reading the response and the usage
The answer is asynchronous. Execute returns straight away and OnExecuted
fires when the exchange — including any tool round trips — has finished.
procedure TForm1.AIExecuted(Sender: TObject; AResponse: TTMSMCPCloudAIResponse;
AHttpStatusCode: Integer; AHttpResult: string);
begin
{ AResponse is nil when the request itself failed. Check it before
touching any of its properties. }
if not Assigned(AResponse) then
begin
memoAnswer.Lines.Text :=
Format('Request failed (%d): %s', [AHttpStatusCode, AHttpResult]);
Exit;
end;
memoAnswer.Lines.Text := AResponse.Content.Text;
{ Per-request accounting, plus what the service actually used - handy when
a provider silently substitutes a model. }
lblRequest.Caption := Format('%s on %s - %d prompt / %d completion / %d total',
[AResponse.ServiceId, AResponse.ServiceModel,
AResponse.PromptTokens, AResponse.CompletionTokens, AResponse.TotalTokens]);
{ Usage accumulates across every request the component performs, including
speech and transcription, until Reset is called. }
lblSession.Caption := Format('Session: %d tokens, %d speech characters',
[AI.Usage.TotalTokens, AI.Usage.AudioCharacters]);
end;
procedure TForm1.btnNewSessionClick(Sender: TObject);
begin
AI.Usage.Reset;
end;
OnExecuted is a
TTMSMCPCloudAIResultEvent. Its
AResponse parameter is nil when the request failed, in which case
AHttpStatusCode and AHttpResult carry the reason. ServiceModel reports the
model the service actually used, which is worth logging — providers do
substitute models.
Usage accumulates PromptTokens,
CompletionTokens, TotalTokens, AudioDuration, and AudioCharacters across
every request until Usage.Reset is called, so a per-session or per-document
budget needs no bookkeeping of your own.
Turn on Logging and set LogFileName while developing: every REST call to the
service is then written out in full, which is usually faster than guessing why a
provider rejected a request.
Discovering services and models at run time
Hard-coding a model string ages badly, and a service with no key configured should not appear in a picker at all.
procedure TForm1.FillServiceList;
begin
{ GetActiveServices returns the component's own internal list - copy its
contents, never take ownership of it and never free it. Assign copies
both the strings and the TTMSMCPCloudAIService value in Objects[]. }
cbService.Items.Assign(AI.GetActiveServices);
if cbService.Items.Count > 0 then
cbService.ItemIndex := 0;
end;
procedure TForm1.cbServiceChange(Sender: TObject);
begin
AI.Service :=
TTMSMCPCloudAIService(Integer(cbService.Items.Objects[cbService.ItemIndex]));
{ Ask the selected service which models it currently offers. The list
arrives asynchronously, so read it in OnGetModels. }
AI.OnGetModels := AIGetModels;
AI.GetModels;
end;
procedure TForm1.AIGetModels(Sender: TObject; AResponse: TTMSMCPCloudAIResponse;
AHttpStatusCode: Integer; AHttpResult: string);
begin
if AHttpStatusCode div 100 <> 2 then
begin
memoAnswer.Lines.Add('Could not list models: ' + AHttpResult);
Exit;
end;
cbModel.Items.Assign(AI.Models);
end;
GetServices(UseFunctionCalling, UseFiles, HasAPIKey) returns the service names
that match the requested capabilities, with the matching
TTMSMCPCloudAIService value in each string's Objects[] entry.
GetActiveServices(UseFunctionCalling, UseFiles) is the same call with
HasAPIKey already set to True, so it lists only services you can actually
reach.
Both return the component's own internal list. Copy the contents with
Assign — never free the result and never keep the reference past the next
call. See Common mistakes.
GetModels asks the selected service what it currently offers; the list lands
in Models and OnGetModels fires when it is ready.
Combining provider selection, roles, settings, and usage
A real request touches all of the above at once. This one picks a reachable
provider, loads a long checklist as the system role, omits the temperature
parameter, caches the checklist when the provider is Claude, and reports the
result through the same OnExecuted handler:
procedure TForm1.RunReviewPass(const ASourceText: string);
var
Services: TStringList;
begin
{ 1. Provider selection - prefer Claude when a key is present, and fall
back to whatever else is configured. GetActiveServices hands back the
component's internal list, so only its contents are copied. }
Services := TStringList.Create;
try
Services.Assign(AI.GetActiveServices);
if Services.IndexOf('Claude') >= 0 then
AI.Service := aiClaude
else if Services.Count > 0 then
AI.Service := TTMSMCPCloudAIService(Integer(Services.Objects[0]))
else
raise Exception.Create('No AI service has an API key configured.');
finally
Services.Free;
end;
{ 2. Roles - a long, stable review checklist as the system role. }
AI.SystemRole.LoadFromFile(ReviewChecklistFileName);
AI.AssistantRole.Text := 'I answer with a numbered list of findings.';
AI.Context.Text := ASourceText;
{ 3. Generation settings - deterministic, bounded, and with the temperature
parameter omitted for models that reject it. }
AI.Settings.UseTemperature := False;
AI.Settings.MaxTokens := 4096;
AI.Settings.Reasoning := aiMedium;
{ Claude only: let the service cache the checklist between turns. }
if AI.Service = aiClaude then
AI.Settings.CacheControl := ccEphemeral;
{ 4. Response and usage are handled in OnExecuted. }
AI.OnExecuted := AIExecuted;
AI.Execute('review');
end;
The id passed to Execute comes back as AResponse.Id, so one OnExecuted
handler can serve several call sites and still tell them apart.
Common mistakes
- Freeing the list returned by
GetServicesorGetActiveServices. Both return the component's internalTStringList, not a copy made for you. Freeing it corrupts the component and the next call will fail. Copy the contents (cbService.Items.Assign(AI.GetActiveServices)orLocal.Assign(...)) and leave the original alone. The same rule applies toModels, toFiles[i], and to theSoundBufferhanded toOnSpeechAudio. - Reading
AResponse.Contentwithout checkingAssigned(AResponse). A failed request callsOnExecutedwithAResponseset toniland the reason inAHttpStatusCode/AHttpResult. - Setting
Temperature := 0for a model that rejects the parameter. The parameter is still sent. UseSettings.UseTemperature := Falseto omit it. - Treating
Executeas synchronous. It returns immediately and itsBooleanresult only says the request was started. Do your work inOnExecuted, and guard the entry point withBusyso a second request does not start on top of the first. - Expecting
CacheControlto help every service. It emits Claude'scache_controlmarker; other services ignore it. It also only pays off for a large, stable block reused across turns. - Leaving stale attachments in
Files.AddFile,AddText, andAddURLappend. CallClearFilesbefore building a new request or the previous turn's documents are sent again — see Files, assistants, speech, and transcription.
See also
- Function calling — let the model call your own code
- Files, assistants, speech, and transcription
- Getting started
TTMSMCPCloudAI,TTMSMCPCloudAISettings,TTMSMCPCloudAIResponse,TTMSMCPCloudAIUsage