Net-Base Tímarit

15.07.2026

ChatGPT API með Delphi FMX/VCL: Áreiðanleg tenging með streymi, endurtilraunum og hreinu JSON-parsing

Svo tengir þú ChatGPT API við Delphi FMX/VCL á traustan hátt: HTTP-viðskiptavinur með tímalokum og endurtilraunum, SSE-streymi án UI-frjósa, og áreiðanleg JSON-greining fyrir verkfæraköll og villumeðhöndlun.

15.07.2026

Frá tímaritsþema til verkefnaframkvæmdar

Viðeigandi þjónustu- og tæknisíður fyrir greinina

Af hverju „ChatGPT API mit Delphi FMX/VCL“ in der Praxis nicht nur ein POST ist

Wer die ChatGPT API mit Delphi FMX/VCL anbinden will, landet schnell bei einem simplen HTTP-POST. In echten Business-Software-Umgebungen kippt das aber an drei Stellen: (1) Timeouts und Retries müssen deterministisch sein, weil Anwender sonst „hängende“ UI erleben, (2) Streaming (Server-Sent Events, kurz SSE) ist für gute UX oft sinnvoll, aber im Delphi-Threading schnell fehleranfällig, und (3) JSON ist nicht nur „ein Objekt“: Fehlermeldungen, Quotenprobleme, leere Felder oder leicht veränderte Antwortformen müssen robust gehandhabt werden.

Der folgende Source-Schnipsel zeigt einen Ansatz, der in FMX und VCL gleichermaßen funktioniert: Ein eigener, testbarer Client, der wahlweise nicht-streaming oder streaming arbeitet, UI-Updates sauber marshalt (also über die Main-Thread-Synchronisation ausführt) und bei Fehlern aussagekräftig loggt. Nebenbei ist er so gebaut, dass er sich in gewachsene Layer-Strukturen einfügt (z. B. „API-Client“ in der Integrationsschicht, UI bleibt dünn).

Architektur-Skizze: UI entkoppeln, Client testbar halten

In Delphi-Projekten mit langer Historie findet man häufig „HTTP im ButtonClick“. Das funktioniert bis zum ersten Incident. Empfehlenswert ist ein kleiner Client mit:

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

So kann die Integration in individuelle Unternehmenssoftware sauber betrieben werden: Der Client lässt sich in Services, Desktop-Clients, Admin-Tools oder Test-Harnesses wiederverwenden.

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-táknið: hér er API-lykillinn sem „Authorization: Bearer …“.
// Í fyrirtækjaumhverfum skal einnig gæta þess að lyklar komist ekki í logg.
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));

// Lágmarksuppbygging ‚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
// Algeng uppbygging: { „error“: { „message“: „…“, „type“: „…“ } }
EObj := (J as TJSONObject).GetValue(‚error‘);
if Assigned(EObj) then
begin
AMessage := EObj.GetValue(‚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(‚Óvænt JSON-svar (ekki JSON-objekt).‘, 0, AJsonText);

Root := J as TJSONObject;
Choices := Root.GetValue(‚choices‘);
if (Choices = nil) or (Choices.Count = 0) then
raise EChatApiError.Create(‚Óvænt JSON-svar: „choices“ vantar/er tómt.‘, 0, AJsonText);

Choice0 := Choices.Items[0] as TJSONObject;
// Chat Completions: choices[0].message.content
Msg := Choice0.GetValue(‚message‘);
if Msg = nil then
raise EChatApiError.Create(‚Óvænt JSON-svar: „message“ vantar.‘, 0, AJsonText);

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

function TOpenAIChatClient.ExecuteWithRetry(const ADoRequest: TFunc): 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
// Net-/TLS-/tímaúrtaksvillur: einföld endurtilraun með backoff.
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-villa ‚ + 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
// Streymi er meðvitað ræst sem ósamstillt til að FMX/VCL blokkist ekki.
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
// Við villur í streymi er innihald (content) oft samt JSON.
TThread.Queue(nil,
procedure
begin
AOnEvent(‚Upphaf streymis misheppnaðist (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;

// SSE-snið: línur eins og „data: {…}“ eða „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;

// Unnið úr JSON-bút: 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(‚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.

Hvers vegna þessi nálgun nýtist

Kóðinn leysir þrjár dæmigerðar vandamálaflokkar sem geta fljótt orðið kostnaðarmiklir í VCL/FMX:

  • Streaming án þess að UI frjósi: HTTP-streymið er lesið í bakgrunni; uppfærslur í UI fara í gegnum TThread.Queue (ósamstilltur inn í aðalþráð). Þetta er í FMX og VCL traustur staðalháttur.
  • Endurtilraun við netvillur: Við ENetHTTPClientException er reynt aftur með stigvaxandi bið (backoff). Þetta er vísvitandi einfalt og hægt að bæta við síðar með tilliti til stöðukóða (429/5xx).
  • Traust JSON-skilun: Í stað þess að gera blindar tegundabreytingar á reitum er farið skref fyrir skref og athugað. Þetta dregur úr „Invalid type cast“-villum við sérsvör.

Skilyrði, gildrur og afbrigði

  • SSE er ekki „venjulegt JSON“: Við streymi kemur röð atburða, ekki ein eining af JSON-svörun. Þess vegna er línuskipt lesið og að þekkja [DONE] grundvallaratriði.
  • THTTPClient og Proxies/SSL: Í stjórnandanetum eru TLS-Inspection og kröfur um proxy raunverulegar. Skipuleggið THTTPClient.ProxySettings og ef þörf er mál er að skoða vottorðsmál. Við villuleit: skráið alltaf statuskóða/headers (án API-Key).
  • Tímaútstefna: ResponseTimeout er viðkvæmt við streymi: Ef valið er of stutt klippir klientinn langar svör. Í UI-tólum er oft skynsamlegra að hafa lengri timeout og „Hætta“-hnapp frekar en „stutt og hart“.
  • Þráðsrof: Sýnisbúturinn sýnir ekki Cancel-Token. Fyrir framleiðslugögn er skynsamlegt að hafa afbrotskerfi (t.d. flagg + Http.CancelAll í nýrri Delphi-útgáfum eða með stjórnuðum streymisbrotum).
  • Líkön og API-þróun: Uppbygging svara getur breyst. Haltu parsaranum varnarvænum og miðlægum, ekki dreift inn í formakóða.

Villuleit í eldri Delphi-viðskiptavinum: Hvað ber raunverulega að skrá?

Í samþættingarverkefnum mistakast fyrstu gangsetningar sjaldnast vegna JSON, heldur vegna umhverfismála. Í tæknilegt logg (skrá, Eventlog, miðlægt skráningarkerfi) ætti í framkvæmd að koma:

  • Request-ID (kennd af ykkur sjálfum), tímamerki, áfangastaður-URL (án leynilegra fyrirspurnarstrengja).
  • HTTP-stöðukóði, Content-Type, svariðlengd, svartími.
  • Styttur Response-Body við villur (t.d. hámark 4–8 KB) til að greina quota-/policy-villum.
  • Skýr merking endurtilrauna: Attempt, Delay, Exception-Klasse.

API-Key á aldrei að fara í loggið. Ef þið skráið Request-Body, þá aðeins í greiningarbyggingum og með maskun, því Prompts geta innihaldið persónu- eða viðskiptaupplýsingar.

Staðsetning fyrir eldri aðstæður: VCL, FMX og Layer-3 arkitektúr

Margar Delphi-umsóknir keyra í klassísku 3‑lagakerfi („Layer-3 Architektur“: UI, viðskipta‑rökhús, gögn/samþætting). Fyrir ChatGPT‑tengingu er þetta gagnlegt: Sýndu klientinn á samþættingarlagi; viðskiptarökhúsið ákveður hvað er spurt; UI birtir aðeins sögu og stöðu. Þannig forðast þú að seinni breytingar (annar veitandi, On‑Prem‑Proxy, nýir endapunktar) rjúfi formin.

Einnig fyrir Delphi-nýtingu er þetta góður inngangur: Upphaflega byggja stöðugan klient, svo bæta UI (Streaming, Hætta, Saga), og síðan innleiða „greindari“ eiginleika eins og uppbyggð svör eða tóla‑köll.

Niðurstaða: Traust undirstaða, en ekki öll forrit þurfa streymi

ChatGPT-API með Delphi FMX/VCL er sérstaklega gagnleg þar sem svörun notendaviðmóts, rekstraröryggi og auðveld bilanagreining skiptir máli: stjórnunarverkfæri, ferlinærar skrifborðsviðskiptavinar eða stuðningsverkfæri í stafrænum fyrirtækjalausnum. Sýndur bútur er meðvitað hagnýtur: SSE-streymi án sértækra bókasafna, endurtilraun aðeins fyrir raunveruleg netbilun, JSON varlega greint.

Takmarkanir notkunar: Ef þið þurfið strangar kröfur um samræmi, miðlæga prompt-stjórnun, stuðning við fjölleigun eða ítarlega endurskoðunarspor dugar „ein viðskiptavinur á skrifborði“ yfirleitt ekki. Þá á tengingin yfirleitt heima á stjórnuðum þjón (t.d. eigin REST-service), sem útfærir stefnu, skráningu og aðgangsstýringu miðlægt. Fyrir margar Delphi-uppsetningar er sýndur viðskiptavinur hins vegar traustur upphafspunktur sem hægt er smám saman að færa yfir í hreinni heildararkitektúr.

Í faglegu samhengi spila einnig Openai API í Delphi og Delphi HTTP-viðskiptavinar biðtími og endurtilraun mikilvægt hlutverk, þegar samþættingar, gagnastreymar og áframhaldandi þróun þurfa að vinna hreint saman.

Ræddu verkefni eða endurnýjunarverkefni með Net-Base.

Næsta skref

Ef efnið verður að raunverulegu verkefni, ætti snemma að skoða kerfisarkitektúr, núverandi kerfi og rekstur í sameiningu.

Við styðjum ekki aðeins við einstakar spurningar, heldur einnig þegar úr kóðabútum, eldri kerfum eða gáttahugmyndum þarf að verða traust fyrirtækjaverkefni.

  • Núverandi staða, markmynd og tæknileg áhætta eru metin saman.
  • REST, aðgangur að gögnum, gáttir og innleiðing verða ekki flutt til síðari tíma sem afleiðingar.
  • Þú sérð snemma hvaða leið er efnahagslega og rekstrarlega framkvæmanleg.

Deila færslu

Deila þessari færslu beint

LinkedIn, X, XING, Facebook, WhatsApp og tölvupóstur eru strax í boði. Fyrir Instagram undirbúum við tengil og stuttan texta strax.

Tölvupóstur

Instagram opnast í nýjum flipa. Tengill og stuttur texti eru afritaðir í klippiborðið á undan.