But the real changes happened under the hood.
I’ve refactored the codebase to fully separate the AI client logic from both the UI and the network layer. And what does that mean for you? It means you can now use the Chatterbox AI libraries directly in your own code.
Let me say that again, loud and clear:
You can now use Chatterbox AI libraries from your code!
To test this, I created a very basic demo called EngineDemo. And naturally, for this announcement, I had to explain how the demo uses the Chatterbox libraries.
Since this was a brand-new development that the AI had no prior knowledge of, I decided to revisit an experiment I mentioned in a previous post—one that was only partially successful. This time, I took a different approach. But more on that in the next post.
For now, take a look at what ChatGPT had to say on the topic. Just keep in mind: this wasn’t a simple “click and publish” process. ChatGPT and I had a long back-and-forth, and the result of that collaboration is the article below.
🔌 Integrating LLM Engines in Your Delphi App with Chatterbox
Chatterbox is a modular framework designed to simplify working with large language models (LLMs) in Delphi. While the primary goal of the Chatterbox library is to abstract away the differences between various providers like OpenAI, Anthropic, and Google Gemini, it does so without making assumptions about your network stack. This gives you the freedom to plug its LLM abstractions into your own application without being locked into a particular HTTP library or protocol.In this article, we'll take a closer look at how to use the Chatterbox units to talk to LLMs directly in your own projects. We'll be using the EngineDemo sample application from the Chatterbox repository as a reference.
🧱 Decoupling Logic from Transport
One important architectural decision in Chatterbox is that the LLM-related units (those under the CB.AI.Client.*
namespace) do not perform any HTTP requests themselves. Instead, they expose methods for generating request payloads and parsing responses.
This design provides an important level of separation: you are free to use Indy
, THTTPClient
, WinHTTP
, or any custom networking solution you like. The engine logic and request structure are handled by Chatterbox, but you're responsible for sending and receiving the HTTP data.
This pattern is clearly visible in the EngineDemo
project, where requests are executed manually using THTTPClient
, and the request/response JSON is delegated to the appropriate serializer.
🧩 Plug-In Architecture via Unit Registration
Chatterbox uses a global registry model for managing supported LLM engines. Each engine is implemented in a dedicated unit under the CB.AI.Client.*
namespace. For instance:
CB.AI.Client.OpenAI
handles OpenAI-compatible engines (like GPT-4, GPT-3.5)CB.AI.Client.Anthropic
supports ClaudeCB.AI.Client.Gemini
supports Google Gemini- ...and so on
Each of these units registers itself with a global engine registry called GSerializers
. When your application starts, only engines whose units are actually linked into the binary will be registered. That means your app supports only what it includes—no more, no less.
You can see this in the EngineDemo.dpr
file:
program EngineDemo;
uses
Vcl.Forms,
engineDemoMain in 'engineDemoMain.pas' {frmEngineDemo},
CB.AI.Client.Anthropic in '..\..\src\CB.AI.Client.Anthropic.pas',
CB.AI.Client.DeepSeek in '..\..\src\CB.AI.Client.DeepSeek.pas',
CB.AI.Client.Gemini in '..\..\src\CB.AI.Client.Gemini.pas',
CB.AI.Client.Ollama in '..\..\src\CB.AI.Client.Ollama.pas',
CB.AI.Client.OpenAI in '..\..\src\CB.AI.Client.OpenAI.pas',
CB.AI.Registry in '..\..\src\CB.AI.Registry.pas',
CB.Settings.Types in '..\..\src\CB.Settings.Types.pas',
CB.AI.Interaction in '..\..\src\CB.AI.Interaction.pas',
CB.Network.Types in '..\..\src\CB.Network.Types.pas',
engineDemo.SelectModelDlg in 'engineDemo.SelectModelDlg.pas' {frmSelectModel};
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TfrmEngineDemo, frmEngineDemo);
Application.Run;
end.
This manual inclusion gives you precise control over which engines your app supports, and it keeps your final binary lean and intentional.
🎛 Selecting the Right Engine
In the main form, the combo box cbxEngineType
lets the user select the engine to use. The OnChange
handler for this combo box wires up the correct serializer from the GSerializers
registry:
FSerializer := GSerializers[engine];
Each serializer implements the IAISerializer
interface and knows how to generate requests and parse responses for its specific engine.
The UI also adjusts based on serializer capabilities. For instance:
btnListModels.Enabled := FSerializer.URL(EngineConfig, qpModels) <> '';
btnGetAPIKey.Enabled := FSerializer.URL(EngineConfig, qpAPIKeys) <> '';
This makes it easy to reflect different capabilities per engine in the interface, using just the metadata exposed by the serializer.
💬 Running a Prompt
To send a user prompt to the LLM, the btnRunQueryClick
handler:
- Builds a request using
FSerializer.QuestionToJSON(...)
- Executes the HTTP request manually using
ExecuteHttp
- Parses the JSON response using
FSerializer.JSONToAnswer(...)
var response := ExecuteHttp(
FSerializer.URL(EngineConfig, qpChat),
FSerializer.QuestionToJSON(EngineConfig, [], false, inpQuery.Text),
MakeHeaders);
var answer := FSerializer.JSONToAnswer(EngineConfig, response.ContentAsString, errorMsg);
This design puts Chatterbox in charge of formatting and interpreting engine-specific JSON but leaves networking in your hands. You decide how requests are sent and how errors are handled.
🔑 Opening the API Key Page
To help users obtain an API key, btnGetAPIKeyClick
uses the qpAPIKeys
query parameter to generate a URL via the serializer:
ShellExecute(0, 'open', PChar(FSerializer.URL(EngineConfig, qpAPIKeys)), nil, nil, SW_SHOWNORMAL);
Each engine defines this link in its CB.AI.Client.*
unit. For example, OpenAI would return a link to https://platform.openai.com/account/api-keys.
📜 Listing Available Models
Clicking the “List Models” button issues a request to the engine's model listing endpoint:
var response := ExecuteHttp(FSerializer.URL(EngineConfig, qpModels), '', MakeHeaders);
The serializer then parses the model list from the JSON response:
var models := FSerializer.JSONToModels(response.ContentAsString, errorMsg);
All currently supported engines implement dynamic model listing. The demo displays the list using a simple modal dialog, and the user’s selection is copied to the input field.
🌐 Implementing the HTTP Layer
Since Chatterbox doesn’t handle HTTP, the demo uses Delphi's built-in THTTPClient
. The ExecuteHttp
function wraps GET and POST logic based on the presence of a body.
Headers are created using the GNetworkHeaderProvider
registry:
for var hdr in GNetworkHeaderProvider[FSerializer.EngineType] do
GNetworkHeaderProvider
, like GSerializers
, is a global registry populated by each CB.AI.Client.*
unit. It provides the headers that need to be included in each request.
Some headers include the placeholder CAuthorizationKeyPlaceholder
, which gets dynamically replaced with the actual API key at runtime:
var hdrValue := StringReplace(hdr.Value2, CAuthorizationKeyPlaceholder, inpAPIKey.Text, []);
This makes it simple to inject credentials without engine-specific hardcoding in your application.
🏁 Wrapping Up
Chatterbox provides a clean, extensible way to interact with LLMs in Delphi. It doesn’t dictate how you manage HTTP, nor does it require you to support every possible engine. Its registry-based design lets you pick and choose, and its serializer interface keeps your code engine-independent.
To get started using Chatterbox in your own projects, simply include the desired CB.AI.Client.*
units and wire up your HTTP layer just like in the EngineDemo
. From there, you'll be generating prompts and parsing replies in no time—with full control over which engines you support and how you communicate with them.
Cool! Another repo on Delphi - OpenAI API integration:
ReplyDeletehttps://github.com/HemulGM/DelphiOpenAI