The presented framework is a comprehensive and extensible solution for Delphi developers looking to integrate modern API calls into their projects, particularly the latest version of OpenAI APIs. This version takes advantage of OpenAI’s latest features while offering increased flexibility through HTTP request mocking, robust unit testing, and smooth JSON parameter configuration.
Key benefits for developers:
-
Integration of OpenAI APIs (latest version): The framework is optimized to interact with the latest OpenAI endpoints, supporting content generation services, language models, and other recent innovations.
-
Mocking HTTP requests: Thanks to the abstraction via the
IHttpClientAPIinterface, developers can easily mock OpenAI API responses without making real network calls. This mechanism is especially useful for unit tests to validate different behaviors, including errors or unexpected responses. -
Unit testing with DUnitX (via the GenAI.API.Tests unit): The framework integrates with DUnitX to allow developers to test various features, such as parameter handling, response deserialization, and error management. The
GenAI.API.Testsunit provides predefined tests covering common scenarios like validating request parameters (TUrlParam), deserializing API objects, and managing errors using exceptions.
For example:
[Test] procedure Test_TUrlParam_AddParameters;
[Test] procedure Test_TGenAIConfiguration_BuildHeaders;
[Test] procedure Test_TApiDeserializer_Deserialize;This structure makes it easy to create additional tests tailored to specific developer needs.
-
Centralized request management: The
TGenAIAPIclass simplifies interaction with OpenAI services through standardized methods (GET, POST, DELETE, PATCH), centralizing the construction of requests and the management of responses. -
Smooth JSON parameter configuration with chaining: The framework introduces a flexible approach to configure JSON request parameters using method chaining. Developers can chain multiple calls to add successive parameters via methods like
Add()in theTJSONParamclass.
Example:
JSONParam.Add('key1', 'value1').Add('key2', 42).Add('key3', True);This approach makes configuring request data more intuitive and fluid.
-
Automatic deserialization of JSON responses: API responses are automatically converted into Delphi objects (
TJSONParam,TAdvancedList, etc.), making them easy to manipulate directly in the code. -
Support for asynchronous operations: Using types like
TAsynDeletion, developers can execute non-blocking API calls, maintaining the overall responsiveness of their applications. -
Flexible request construction and pagination support: Classes like
TUrlParamandTUrlPaginationParamsallow developers to dynamically configure complex requests with options for pagination, sorting, and filtering. -
Robust error management: The framework includes detailed error handling through specific exceptions (
TGenAIAPIException,TGenAIAuthError, etc.), making it possible to capture and handle errors related to authentication, quotas, or server responses effectively.
Conclusion
This framework aims to provide a practical and efficient solution for integrating OpenAI APIs into Delphi projects. The support for method chaining in JSON request configuration, combined with unit testing (via DUnitX and the GenAI.API.Tests unit) and flexible error handling, enables developers to focus on the core business logic of their applications. Although it’s not exhaustive, this framework is designed to evolve with developers’ needs and the technological advancements it supports.
This Delphi project relies on several key dependencies that cover network functionality, JSON handling, serialization, asynchronous operations, and error management. Here are the main categories of dependencies:
-
Standard Delphi Dependencies: Utilizes native libraries such as System.Classes, System.SysUtils, System.JSON, and System.Net.HttpClient for general operations, input/output, date management, and network communications.
-
JSON and REST: Uses units like REST.Json.Types, REST.Json.Interceptors, and REST.JsonReflect to handle object serialization/deserialization and REST API calls.
-
Custom Exception and Error Handling: Internal modules GenAI.Exceptions and GenAI.Errors capture and propagate errors specific to the API.
-
Custom GenAI API Modules: Custom modules like GenAI.API, GenAI.API.Params, and GenAI.HttpClientInterface are used to build HTTP requests to the GenAI API and handle asynchronous responses.
-
Multithreading and Asynchronous Operations: Utilizes System.Threading and internal classes (such as TAsynCallBack) to handle long running tasks and avoid blocking the main thread.
-
Testing Dependencies: Uses DUnitX.TestFramework and related modules to implement unit tests and validate critical project functionality.
This project is structured to be modular and extensible, with abstractions that allow for easily switching network libraries or adding new features while maintaining robustness and testability.
The proposed architecture aims to facilitate the management of parameters and the execution of asynchronous operations, particularly for chat requests. Two main units are used:
- GenAI.Async.Params: Provides generic interfaces and classes to manage parameters flexibly and in a reusable manner.
- GenAI.Async.Support: Defines records and classes to control the lifecycle of asynchronous operations, particularly for chat or streaming-based tasks.
The goal is to separate the logic for managing parameters from the logic for asynchronous execution, while ensuring proper synchronization with the main thread (GUI) through callbacks.
This generic interface allows for managing parameters of type T, with the following key methods:
- SetParams/GetParams: To set and retrieve the parameter values.
- Assign: Allows assigning values using a function (of type
TFunc<T>). - AsSender: Returns the instance as a
TObject, useful for identifying the sender during asynchronous execution.
Implements the IUseParams<T> interface and encapsulates internal parameter management through a private variable FParams. This provides a simple abstraction for storing and manipulating the parameters required for asynchronous operations.
This static factory class creates instances of IUseParams<T>. Two creation methods are provided:
- One method without parameters that creates an empty instance.
- One method that accepts a function of type
TFunc<T>to initialize the parameters during creation.
Advantage: Using generics makes it possible to reuse the same mechanism for different parameter types, making the code highly flexible and easily extensible.
The TAsynCallBackExec<T, U> class is the core of asynchronous execution. It combines parameter management with asynchronous task execution through the following components:
-
Initialization: The constructor receives a function to obtain the parameters (of type
TFunc<T>). These parameters are encapsulated via anIUseParams<T>instance created by the factory. -
Method
Run: This method accepts a function (TFunc<U>) representing the operation to be executed asynchronously. Key points of its functionality include:- Assigning Internal Callbacks: Before starting the task, the callbacks (
OnStart,OnSuccess,OnError) and the sender are assigned to local variables. This avoids concurrency issues or unexpected changes during background execution. - Creating and Starting a
TTask: The method usesTTask.Createto wrap the operation for background execution. UsingTTaskenables parallelism without blocking the main thread. - Synchronizing with the Main Thread: To interact with the user interface or ensure that callbacks are executed in the context of the main thread,
TThread.Queueis used.- OnStart: Triggered before executing the function.
- OnSuccess: Triggered with the operation result upon completion.
- OnError: Triggered with an error message if an exception occurs.
- Exception Handling: The asynchronous operation is wrapped in a
try...exceptblock. If an exception occurs, the exception object is captured, and its message is passed to theOnErrorcallback. Special care is taken to free the exception (usingError.Free) to avoid memory leaks. - Resource Management: In the OnSuccess callback, the result (if it is a dynamically allocated object) is freed after processing to ensure proper memory management.
- Assigning Internal Callbacks: Before starting the task, the callbacks (
In addition to standard execution, the unit also provides a TAsynStreamCallBack record for managing streaming chat requests.
This record defines several events:
- OnStart, OnSuccess, OnProgress, OnError, OnCancellation: Manage the beginning, success, progress, errors, and cancellation of a streaming operation.
- OnDoCancel: A function that periodically checks whether the operation should be canceled.
This mechanism enables progressive handling of responses from the model (e.g., when generating tokens during a conversation).
The asynchronous mechanism leverages generics, non-blocking execution, robust exception handling, and a callback-based structure to provide a flexible, reusable, and maintainable solution for managing asynchronous tasks while ensuring safe and responsive user interfaces.
In this project, I chose to use the IHttpClientAPI interface to handle HTTP requests instead of calling a specific class like THttpClient directly. This brings more flexibility, makes testing easier, and ensures the code is cleaner and more maintainable.
Instead of having code that directly depends on a specific HTTP implementation (like THttpClient), we rely on an interface that defines the necessary actions (GET, POST, DELETE…). We then inject the implementation we want to use. This decouples the business logic from the technical details.
- Easily swap out the HTTP client: If I ever want to replace
THttpClientwith another HTTP library or a new version, I won’t have to rewrite everything. I just need to create a new implementation that follows theIHttpClientAPIinterface, and that’s it. - Simplified unit testing: During testing, I can inject a mock version of the interface. There’s no need to make real HTTP requests or rely on an external server. I can easily simulate network errors, specific responses (e.g., 500 errors), and test different scenarios.
- Cleaner and scalable code: The business logic doesn’t care how HTTP requests are made; it only focuses on what to do with the response. As a result, the code is more readable, easier to maintain, and adheres to the Dependency Inversion Principle from SOLID.
- I defined an interface
IHttpClientAPIthat specifies what the HTTP client should be able to do. - I created a concrete implementation (e.g.,
THttpClientAPI) that handles the actual HTTP requests. - The code interacting with the API only uses the interface, without knowing the underlying implementation.
- During execution or testing, I inject the desired implementation (real or mock).
- If the API changes or I switch to a different networking technology, I won’t break the entire codebase.
- Tests are fast and reliable because I can simulate network behaviors without relying on external infrastructure.
- The code remains modular, clean, and easy to maintain over time.
In summary: This small decision helps prevent bigger problems down the line while making it easy to evolve the project.