Net-Base Magazín

15.07.2026

ChatGPT API s Delphi FMX/VCL: robustné napojenie so streamingom, opakovanými pokusmi (retry) a čistým parsovaním JSON

Ako robustne integrovať ChatGPT API pomocou Delphi FMX/VCL: HTTP klient s časovými limitmi a opakovanými pokusmi, SSE streamovanie bez zamŕzania používateľského rozhrania (UI) a spoľahlivé JSON parsovanie pre volania nástrojov a chybové prípady.

15.07.2026

Od témy magazínu k projektovej praxi

Súvisiace stránky služieb a technológií k príspevku

Prečo „ChatGPT API mit Delphi FMX/VCL“ v praxi nie je iba POST

Kto chce pripojiť ChatGPT API mit Delphi FMX/VCL, často skončí pri jednoduchom HTTP-POST. V reálnych business softvérových prostrediach sa to však láme na troch miestach: (1) Timeouts a Retries musia byť deterministické, inak používatelia zažijú „zavesené“ UI, (2) Streaming (Server-Sent Events, skrátene SSE) je pre dobrú UX často rozumný, ale v Delphi-Threadingu rýchlo náchylný na chyby, a (3) JSON nie je len „jeden objekt“: chybové hlásenia, problémy s kvótami, prázdne polia alebo mierne zmenené tvary odpovedí musia byť robustne spracované.

Nasledujúci Source-Schnipsel ukazuje prístup, ktorý funguje rovnako vo FMX aj VCL: vlastný, testovateľný klient, ktorý môže pracovať voliteľne v režime not-streaming alebo streaming, správne marshaluje UI-aktualizácie (t. j. vykonáva ich cez synchronizáciu hlavného vlákna) a pri chybách dáva výpovednú stopu v logu. Zároveň je navrhnutý tak, aby sa dal vložiť do existujúcich vrstvových štruktúr (napr. „API-Client“ v integračnej vrstve, UI zostáva tenké).

Architektur-Skizze: UI entkoppeln, Client testbar halten

V Delphi-projektoch s dlhou históriou sa často nájde „HTTP im ButtonClick“. To funguje až do prvého incidentu. Odporúčateľné je malý klient s:

  • Konfiguration: Base-URL, API-Key, Modell, Timeouts.
  • Transportschicht: HTTP-Request/Response, Retry, Timeout, Proxy/SSL-Optionen (je nach Betrieb).
  • Parser: JSON-Decoding, Fehlerobjekte, Ergebnisextraktion.
  • UI-Hooks: Callback für Token-/Text-Streaming, aber ohne harte Abhängigkeit auf VCL/FMX Controls.

Týmto spôsobom je integrácia do individuálneho podnikového softvéru čistá: klient je možné znovu použiť v servisoch, desktop-klientoch, administračných nástrojoch alebo test-harnesses.

Source-Schnipsel: Delphi-Client mit SSE-Streaming, Timeout/Retry und robustem JSON

Der Code nutzt THTTPClient (System.Net.HttpClient) und pars(t) bewusst nur minimalistisch mit System.JSON. Für SSE wird zeilenweise gelesen und auf „data: …“ reagiert. Das ist kein „WebSocket“, sondern ein HTTP-Response-Stream, der kontinuierlich Textzeilen liefert. Wichtig: Wir lesen in einem Worker-Thread und marshalen UI-Updates in den Main Thread.

unit Net-Base.OpenAI.ChatClient;

interface

uses
System.SysUtils, System.Classes, System.Net.URLClient, System.Net.HttpClient,
System.Net.HttpClientComponent, System.JSON, System.Threading,
System.SyncObjs;

type
EChatApiError = class(Exception)
private
FHttpStatus: Integer;
FResponseText: string;
public
constructor Create(const Msg: string; AHttpStatus: Integer; const AResponseText: string);
property HttpStatus: Integer read FHttpStatus;
property ResponseText: string read FResponseText;
end;

TChatStreamEvent = reference to procedure(const AChunkText: string; AIsFinal: Boolean);

TChatCompletionOptions = record
Model: string;
Temperature: Double;
MaxTokens: Integer;
constructor Create(const AModel: string; ATemperature: Double = 0.2; AMaxTokens: Integer = 512);
end;

TOpenAIChatClient = class
private
FBaseUrl: string;
FApiKey: string;
FConnectTimeoutMs: Integer;
FResponseTimeoutMs: Integer;

function BuildChatRequestBody(const AUserPrompt: string; const AOptions: TChatCompletionOptions;
AStream: Boolean): TJSONObject;

function ExtractTextFromNonStreamingResponse(const AJsonText: string): string;
function TryExtractErrorMessage(const AJsonText: string; out AMessage: string): Boolean;

procedure ApplyAuthHeaders(ARequest: IHTTPRequest);

function ExecuteWithRetry(const ADoRequest: TFunc): IHTTPResponse;
public
constructor Create(const ABaseUrl, AApiKey: string);

property ConnectTimeoutMs: Integer read FConnectTimeoutMs write FConnectTimeoutMs;
property ResponseTimeoutMs: Integer read FResponseTimeoutMs write FResponseTimeoutMs;

function ChatOnce(const AUserPrompt: string; const AOptions: TChatCompletionOptions): string;

procedure ChatStream(const AUserPrompt: string; const AOptions: TChatCompletionOptions;
const AOnEvent: TChatStreamEvent);
end;

implementation

{ EChatApiError }

constructor EChatApiError.Create(const Msg: string; AHttpStatus: Integer; const AResponseText: string);
begin
inherited Create(Msg);
FHttpStatus := AHttpStatus;
FResponseText := AResponseText;
end;

{ TChatCompletionOptions }

constructor TChatCompletionOptions.Create(const AModel: string; ATemperature: Double; AMaxTokens: Integer);
begin
Model := AModel;
Temperature := ATemperature;
MaxTokens := AMaxTokens;
end;

{ TOpenAIChatClient }

constructor TOpenAIChatClient.Create(const ABaseUrl, AApiKey: string);
begin
inherited Create;
FBaseUrl := ABaseUrl.TrimRight([‚/‘]);
FApiKey := AApiKey;
FConnectTimeoutMs := 8000;
FResponseTimeoutMs := 60000;
end;

procedure TOpenAIChatClient.ApplyAuthHeaders(ARequest: IHTTPRequest);
begin
// Bearer token: tu API-kľúč ako „Authorization: Bearer …“.
// V podnikových prostrediach dbajte, aby sa kľúče nedostali do logov.
ARequest.AddHeader(‚Authorization‘, ‚Bearer ‚ + FApiKey);
ARequest.AddHeader(‚Content-Type‘, ‚application/json‘);
ARequest.AddHeader(‚Accept‘, ‚application/json‘);
end;

function TOpenAIChatClient.BuildChatRequestBody(const AUserPrompt: string;
const AOptions: TChatCompletionOptions; AStream: Boolean): TJSONObject;
var
Msgs: TJSONArray;
Msg: TJSONObject;
begin
Result := TJSONObject.Create;
Result.AddPair(‚model‘, AOptions.Model);
Result.AddPair(‚temperature‘, TJSONNumber.Create(AOptions.Temperature));
Result.AddPair(‚max_tokens‘, TJSONNumber.Create(AOptions.MaxTokens));
Result.AddPair(’stream‘, TJSONBool.Create(AStream));

// Minimálna štruktúra messages (Chat Completions): role/content.
Msgs := TJSONArray.Create;
Msg := TJSONObject.Create;
Msg.AddPair(‚role‘, ‚user‘);
Msg.AddPair(‚content‘, AUserPrompt);
Msgs.AddElement(Msg);
Result.AddPair(‚messages‘, Msgs);
end;

function TOpenAIChatClient.TryExtractErrorMessage(const AJsonText: string; out AMessage: string): Boolean;
var
J: TJSONValue;
EObj: TJSONObject;
begin
Result := False;
AMessage := “;

J := TJSONObject.ParseJSONValue(AJsonText);
try
if (J is TJSONObject) then
begin
// Bežný tvar: { „error“: { „message“: „…“, „type“: „…“ } }
EObj := (J as TJSONObject).GetValue<TJSONObject>(‚error‘);
if Assigned(EObj) then
begin
AMessage := EObj.GetValue<string>(‚message‘, “);
Result := AMessage <> “;
end;
end;
finally
J.Free;
end;
end;

function TOpenAIChatClient.ExtractTextFromNonStreamingResponse(const AJsonText: string): string;
var
J: TJSONValue;
Root: TJSONObject;
Choices: TJSONArray;
Choice0: TJSONObject;
Msg: TJSONObject;
begin
Result := “;

J := TJSONObject.ParseJSONValue(AJsonText);
try
if not (J is TJSONObject) then
raise EChatApiError.Create(‚Neočakávaná JSON-odpoveď (nie je objekt).‘, 0, AJsonText);

Root := J as TJSONObject;
Choices := Root.GetValue<TJSONArray>(‚choices‘);
if (Choices = nil) or (Choices.Count = 0) then
raise EChatApiError.Create(‚Neočakávaná JSON-odpoveď: choices chýba alebo je prázdne.‘, 0, AJsonText);

Choice0 := Choices.Items[0] as TJSONObject;
// Chat Completions: choices[0].message.content
Msg := Choice0.GetValue<TJSONObject>(‚message‘);
if Msg = nil then
raise EChatApiError.Create(‚Neočakávaná JSON-odpoveď: message chýba.‘, 0, AJsonText);

Result := Msg.GetValue<string>(‚content‘, “);
finally
J.Free;
end;
end;

function TOpenAIChatClient.ExecuteWithRetry(const ADoRequest: TFunc<IHTTPResponse>): IHTTPResponse;
const
MaxAttempts = 3;
var
Attempt: Integer;
DelayMs: Integer;
begin
DelayMs := 350;
for Attempt := 1 to MaxAttempts do
begin
try
Exit(ADoRequest());
except
on E: ENetHTTPClientException do
begin
// Chyby siete/TLS/timeout: jednoduché opakovanie s backoffom.
if Attempt = MaxAttempts then
raise;
Sleep(DelayMs);
DelayMs := DelayMs * 2;
end;
end;
end;
Result := nil;
end;

function TOpenAIChatClient.ChatOnce(const AUserPrompt: string; const AOptions: TChatCompletionOptions): string;
var
Http: THTTPClient;
Req: IHTTPRequest;
Resp: IHTTPResponse;
Body: TJSONObject;
Payload: TStringStream;
RespText: string;
ErrMsg: string;
begin
Http := THTTPClient.Create;
try
Http.ConnectionTimeout := FConnectTimeoutMs;
Http.ResponseTimeout := FResponseTimeoutMs;

Body := BuildChatRequestBody(AUserPrompt, AOptions, False);
try
Payload := TStringStream.Create(Body.ToJSON, TEncoding.UTF8);
try
Req := Http.GetRequest(‚POST‘, FBaseUrl + ‚/v1/chat/completions‘);
ApplyAuthHeaders(Req);

Resp := ExecuteWithRetry(
function: IHTTPResponse
begin
Result := Http.Execute(Req, Payload);
end
);

RespText := Resp.ContentAsString(TEncoding.UTF8);

if (Resp.StatusCode < 200) or (Resp.StatusCode >= 300) then
begin
if TryExtractErrorMessage(RespText, ErrMsg) then
raise EChatApiError.Create(ErrMsg, Resp.StatusCode, RespText)
else
raise EChatApiError.Create(‚HTTP chyba ‚ + Resp.StatusCode.ToString, Resp.StatusCode, RespText);
end;

Result := ExtractTextFromNonStreamingResponse(RespText);
finally
Payload.Free;
end;
finally
Body.Free;
end;
finally
Http.Free;
end;
end;

procedure TOpenAIChatClient.ChatStream(const AUserPrompt: string; const AOptions: TChatCompletionOptions;
const AOnEvent: TChatStreamEvent);
var
Task: ITask;
begin
// Streamovanie zámerne spúšťame asynchrónne, aby FMX/VCL nebolo blokované.
Task := TTask.Run(
procedure
var
Http: THTTPClient;
Req: IHTTPRequest;
Resp: IHTTPResponse;
Body: TJSONObject;
Payload: TStringStream;
Stream: TStream;
Reader: TStreamReader;
Line, Data: string;
J: TJSONValue;
Delta, Choice0, Choices: TJSONValue;
ContentChunk: string;
begin
Http := THTTPClient.Create;
try
Http.ConnectionTimeout := FConnectTimeoutMs;
Http.ResponseTimeout := FResponseTimeoutMs;

Body := BuildChatRequestBody(AUserPrompt, AOptions, True);
try
Payload := TStringStream.Create(Body.ToJSON, TEncoding.UTF8);
try
Req := Http.GetRequest(‚POST‘, FBaseUrl + ‚/v1/chat/completions‘);
ApplyAuthHeaders(Req);
Req.AddHeader(‚Accept‘, ‚text/event-stream‘);

Resp := ExecuteWithRetry(
function: IHTTPResponse
begin
Result := Http.Execute(Req, Payload);
end
);

if (Resp.StatusCode < 200) or (Resp.StatusCode >= 300) then
begin
// Pri chybách pri streamovaní je obsah často stále v JSON formáte.
TThread.Queue(nil,
procedure
begin
AOnEvent(‚Spustenie streamovania zlyhalo (HTTP ‚ + Resp.StatusCode.ToString + ‚).‘, True);
end);
Exit;
end;

Stream := Resp.ContentStream;
Reader := TStreamReader.Create(Stream, TEncoding.UTF8, True, 4096, False);
try
while not Reader.EndOfStream do
begin
Line := Reader.ReadLine;
if Line = “ then
Continue;

// Formát SSE: riadky ako „data: {…}“ alebo „data: [DONE]“
if Line.StartsWith(‚data:‘) then
begin
Data := Line.Substring(5).Trim;
if SameText(Data, ‚[DONE]‘) then
begin
TThread.Queue(nil,
procedure
begin
AOnEvent(“, True);
end);
Break;
end;

// Vyhodnotenie JSON-fragmentu: choices[0].delta.content
J := TJSONObject.ParseJSONValue(Data);
try
ContentChunk := “;
if J <> nil then
begin
Choices := (J as TJSONObject).GetValue(‚choices‘);
if (Choices is TJSONArray) and (TJSONArray(Choices).Count > 0) then
begin
Choice0 := TJSONArray(Choices).Items[0];
Delta := (Choice0 as TJSONObject).GetValue(‚delta‘);
if (Delta is TJSONObject) then
ContentChunk := TJSONObject(Delta).GetValue<string>(‚content‘, “);
end;
end;
finally
J.Free;
end;

if ContentChunk <> “ then
TThread.Queue(nil,
procedure
begin
AOnEvent(ContentChunk, False);
end);
end;
end;
finally
Reader.Free;
end;
finally
Payload.Free;
end;
finally
Body.Free;
end;
finally
Http.Free;
end;
end);
end;

end.

Na čo je tento prístup dobrý

Kód rieši tri typické triedy problémov, ktoré sa vo VCL/FMX rýchlo môžu stať nákladnými:

  • Streaming bez zamŕzania UI: HTTP-stream sa číta na pozadí; aktualizácie UI prebiehajú cez TThread.Queue (asynchrónne do hlavného vlákna). To je vo FMX a VCL robustný štandardný postup.
  • Retry pri chybách siete: Pri ENetHTTPClientException sa opakuje s backoff mechanizmom. Je to zámerne jednoduché a neskôr sa dá rozšíriť o kontrolu stavových kódov (429/5xx).
  • Robustné parsovanie JSON: Namiesto slepého pretypovávania polí sa postupne kontroluje. To znižuje chyby „Invalid type cast“ pri špeciálnych odpovediach.

Podmienky, úskalia a varianty

  • SSE nie je „bežné JSON“: Pri streamovaní prichádza sled udalostí, nie jedna jediná JSON-odpoveď. Preto je čítanie po riadkoch a rozpoznanie [DONE] kľúčové.
  • THTTPClient a Proxies/SSL: V administrátorských sieťach sú reálne TLS-inspekcia a povinné proxy. Naplánujte THTTPClient.ProxySettings a prípadne otázky certifikátov. Ladenie: Stavový kód/hlavičky vždy logujte (bez API-Key).
  • Strategia timeoutu: ResponseTimeout je pri streamovaní citlivý: Ak zvolíte príliš krátky, klient preruší dlhé odpovede. V UI-nástrojoch je často rozumnejší dlhší timeout a tlačidlo „Abbrechen“ než krátky a tvrdý časový limit.
  • Prerušenie vlákna: Úryvok neukazuje Cancel-Token. Pre produkčné nástroje sa oplatí implementovať mechanizmus prerušenia (napr. flag + Http.CancelAll v novších Delphi-verziách alebo cez kontrolované prerušenie streamu).
  • Vývoj modelu a API: Štruktúra odpovedí sa môže meniť. Držte parser defensívne a centrálne, nie rozptýlený v kóde formulárov.

Ladenie v existujúcich Delphi-klientoch: čo by ste skutočne mali logovať

V integračných projektoch prvé nasadenie zriedka zlyhá kvôli JSON, skôr kvôli detailom prostredia. Do technického logu (súbor, Eventlog, centrálny logger) patria v praxi:

  • Request-ID (vlastne pridelená), časová pečiatka, cieľová URL (bez Secret-Querystrings).
  • HTTP-stavový kód, Content-Type, dĺžka odpovede, doba trvania.
  • Skrátené telo odpovede pri chybách (napr. max. 4–8 KB), aby bolo možné rozpoznať Quota-/Policy-chyby.
  • Explicitné označenie pokusov o retry: Attempt, Delay, Exception-Klasse.

API-Key nikdy nepatrí do logu. Ak logujete Request-Body, robte to len v diagnostických buildoch a s maskovaním, pretože prompty môžu obsahovať osobné alebo obchodné údaje.

Zaradenie pre Legacy-situácie: VCL, FMX a Layer-3 architektúra

Mnoho Delphi-aplikácií beží v klasickej 3-vrstvovej logike („Layer-3 Architektur“: UI, obchodná logika, dáta/integrácia). Pre napojenie ChatGPT je to užitočné: Ukázaný klient patrí do integračnej vrstvy; obchodná logika rozhoduje, čo sa má pýtať; UI zobrazuje iba priebeh a stav. Tým zabránite, aby neskoršia zmena (iný provider, On-Prem-Proxy, nové endpointy) formuláre „roztrhala“.

Aj pre modernizáciu Delphi je to dobrý východiskový bod: Najprv stabilný klient, potom vylepšenia UI (Streaming, Abbrechen, Verlauf), a až potom „inteligentnejšie“ funkcie ako štruktúrované odpovede alebo volania nástrojov.

Záver: Pevný základ, ale nie každá aplikácia potrebuje Streaming

Integrácia ChatGPT API s Delphi do FMX/VCL sa obzvlášť oplatí tam, kde sú dôležité rýchlosť reakcie UI, prevádzková spoľahlivosť a možnosť ladenia: administračné nástroje, procesne blízke desktopové klienty alebo podporné nástroje v digitálnych podnikových riešeniach. Ukážkový útržok je zámerne pragmatický: SSE-Streaming bez špeciálnych knižníc, Retry iba pri skutočných sieťových chybách, JSON parsovaný defenzívne.

Obmedzenia nasadenia: Ak potrebujete prísne požiadavky na compliance, centrálnu správu promptov, podporu viacerých nájomníkov alebo detailné auditné stopy, zvyčajne nestačí „klient na desktope“. V takom prípade patrí integrácia typicky do kontrolovaného servera (napr. vlastný REST-Service), ktorý centrálne implementuje politiky, logging a riadenie prístupu. Pre mnohé inštalácie Delphi je však tu ukázaný klient spoľahlivým východiskovým bodom, ktorý sa postupne dá previesť do čistejšej celkovej architektúry.

V odbornom kontexte zohrávajú dôležitú úlohu aj Openai API v Delphi a Delphi Http Client Timeout Retry, keď musia integrácie, dátové toky a ďalší vývoj hladko spolupracovať.

Projekt alebo modernizačný zámer prekonzultovať s Net-Base.

Nächster Schritt

Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb früh zusammen betrachtet werden.

Podporujeme nielen pri jednotlivých otázkach, ale aj vtedy, keď sa z fragmentov zdrojového kódu, tém súvisiacich s legacy systémami alebo nápadov na portál má stať robustný podnikový projekt.

  • Stav, cieľový obraz a technické riziká sa hodnotia spoločne.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
  • Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

Zdieľať príspevok

Tento príspevok priamo zdieľať

LinkedIn, X, XING, Facebook, WhatsApp und E-Mail sind sofort verfügbar. Für Instagram bereiten wir Link und Kurztext direkt vor.

E-mail

Instagram sa otvorí v novej karte. Odkaz a krátky text sa predtým skopírujú do schránky.