Net-Base Magasin

15.07.2026

ChatGPT API med Delphi FMX/VCL: Robust tilknyting med streaming, retry og korrekt JSON-parsing

Slik knyter du ChatGPT API-en til Delphi FMX/VCL robust: HTTP-klient med timeoutar og gjenforsøk, SSE-streaming utan UI-frys, samt robust JSON-parsing for verktøykall og feilsituasjonar.

15.07.2026

Frå magasinetema til prosjektpraksis

Passande teneste- og tekniske sider til innlegget

Kvifor «ChatGPT API med Delphi FMX/VCL» i praksis ikkje berre er eit POST

Den som vil koble til ChatGPT API med Delphi FMX/VCL, hamnar fort på eit enkelt HTTP-POST. I ekte bedriftsprogramvaremiljøer blir dette likevel problematisk på tre område: (1) Timeouts og Retries må vere deterministiske, fordi brukarar elles opplever eit «hengande» UI, (2) Streaming (Server-Sent Events, kort SSE) er ofte nyttig for god UX, men i Delphi-trådinga blir det raskt feilutsatt, og (3) JSON er ikkje berre «eit objekt»: Feilmeldingar, kvoteproblem, tomme felt eller lett endra svarformat må handterast robust.

Følgjande kildekodestykkje viser ein tilnærming som fungerer likt i FMX og VCL: ein eigen, testbar klient som anten jobbar i ikkje-streaming eller streaming-modus, marshalar UI-oppdateringar reint (det vil seie utfører dei via hovudtrådssynkronisering) og loggar tydelege feilmeldingar. I tillegg er han bygd for å gli inn i eksisterande lagstrukturar (t.d. «API-Client» i integrasjonslaget, UI held seg tynn).

Arkitekturskisse: Løysne UI, halde klienten testbar

I Delphi-prosjekt med lang historie finn ein ofte «HTTP im ButtonClick». Det fungerer til første hending. Anbefalt er ein liten klient med:

  • Konfigurasjon: Base-URL, API-Key, Modell, Timeouts.
  • Transportlag: HTTP-Request/Response, Retry, Timeout, Proxy/SSL-Optionen (avhengig av drift).
  • Parser: JSON-dekoding, feilobjekt, resultatuttrekking.
  • UI-Hooks: Callback for token-/tekst-streaming, men utan hard avhengigheit av VCL/FMX-kontroller.

Slik kan integrasjonen i individuell bedriftsprogramvare drivast ryddig: Klienten kan gjenbrukast i tenester, desktop-klientar, admin-verktøy eller test-harnessar.

Kodeeksempel: Delphi-Client med SSE-Streaming, Timeout/Retry und robustem JSON

Koden nyttar THTTPClient (System.Net.HttpClient) og parsar medvite berre minimalistisk med System.JSON. For SSE blir det lesen linjevis og reagert på «data: …». Dette er ikkje ein «WebSocket», men ein HTTP-respons-strøm som kontinuerleg leverer tekstliner. Viktig: Vi les i ein worker-tråd og marshalar UI-oppdateringar til hovudtråden.

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: her API-nøkkelen som ‚Authorization: Bearer …‘.
// I bedriftsmiljø må ein òg passe på at nøkkelane ikkje hamnar i loggen.
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));

// Minimal oppbygging av 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
// Vanleg form: { „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(‚Uventa JSON-svar (ikkje eit objekt).‘, 0, AJsonText);

Root := J as TJSONObject;
Choices := Root.GetValue(‚choices‘);
if (Choices = nil) or (Choices.Count = 0) then
raise EChatApiError.Create(‚Uventa JSON-svar: choices manglar/er tomt.‘, 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(‚Uventa JSON-svar: message manglar.‘, 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
// Nettverk-/TLS-/timeout-feil: enkel retry med 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-Fehler ‚ + 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
// Startar streaming medvite asynkront for å unngå at FMX/VCL blir blokkert.
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
// Ved streaming-feil er innhaldet ofte framleis JSON.
TThread.Queue(nil,
procedure
begin
AOnEvent(‚Oppstart av streaming feila (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-format: linjer som „data: {…}“ eller „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;

// Tolk JSON-chunk: 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.

Kva denne tilnærminga er god for

Koden løyser tre typiske problemklassar som raskt blir dyre i VCL/FMX:

  • Streaming utan UI-freezes: HTTP-strømmen blir lesen i bakgrunnen; UI-oppdateringar køyrer via TThread.Queue (asynkront i hovudtråden). Dette er i FMX og VCL den robuste standardmåten.
  • Omforsøk ved nettverksfeil: Ved ENetHTTPClientException blir det forsøkt på nytt med backoff. Dette er medvite enkelt og kan seinare utvidast med statuskodar (429/5xx).
  • Robust JSON-parsing: I staden for å caste felt blint, blir dei sjekka trinnvis. Det reduserer «Invalid type cast»-feil ved avvikande svar.

Rammevilkår, fallgruver og variantar

  • SSE er ikkje eit «vanleg JSON»: Ved streaming kjem ei rekkje hendingar, ikkje eitt enkelt JSON-svar. Difor er linjevis lesing og gjenkjenning av [DONE] sentralt.
  • THTTPClient og proxyar/SSL: I administrasjonsnettverk er TLS-inspeksjon og krav til proxy reelle. Planlegg THTTPClient.ProxySettings og eventuelle sertifikatproblem. Feilsøking: logg alltid statuskode/headers (utan API-Key).
  • Timeout-strategi: ResponseTimeout er vanskeleg ved streaming: Vel du for kort, kan klienten avkappa lange svar. I UI-verktøy er lengre timeout og ein «Avbryt»-knapp ofte meir fornuftig enn ein «kort og hard» tilnærming.
  • Tråd-avbrot: Kodeeksemplet viser ikkje ein Cancel-Token. For produksjonsverktøy lønner det seg med ein avbrotsmekanisme (t.d. flagg + Http.CancelAll i nyare Delphi-versjonar eller via kontrollert strømafbrot).
  • Modell- og API-vidareutvikling: Strukturen i responsar kan endre seg. Halde parserar defensive og sentrale, ikkje spreidd i skjema-kode.

Feilsøking i etablerte Delphi-klientar: Kva du verkeleg bør logge

I integrasjonsprosjekt feilar første oppstart sjeldan på JSON, men på miljødetaljar. I ein teknisk logg (fil, Eventlog, sentral logger) høyrer i praksis følgjande heime:

  • Request-ID (tildelt av deg), tidsstempel, mål-URL (utan hemmelege query-strengar).
  • HTTP-statuskode, Content-Type, responslengd, kjøretid.
  • Ein avkorta Response-Body ved feil (t.d. maks 4–8 KB), for å kunne identifisere quota-/policy-feil.
  • Tydelig merking av omforsøk: Attempt, Delay, Exception-klasse.

API-Key høyrer aldri i loggen. Dersom du loggar Request-Body, berre i diagnose-builds og med masking, fordi prompts kan innehalde personopplysningar eller forretningsmessig sensitivt innhald.

Innplassering for legacy-situasjonar: VCL, FMX og Layer-3-arkitektur

Mange Delphi-applikasjonar køyrer i ein klassisk 3-lags logikk («Layer-3 arkitektur»: UI, forretningslogikk, data/integrasjon). For ChatGPT-tilkopling er dette nyttig: Den viste klienten høyrer i integrasjonssjiktet; forretningslogikken avgjer kva som blir etterspurt; UI viser berre historikk og status. Slik unngår du at eit seinare byte (annen leverandør, On-Prem-Proxy, nye endepunkt) riv Forms i stykker.

Også for Delphi-modernisering er dette eit godt startpunkt: Først ein stabil klient, så UI-forbetringar (streaming, avbryt, historikk), og fyrst deretter «intelligente» funksjonar som strukturerte svar eller tool-kall.

Konklusjon: Solid basis, men ikkje alle applikasjonar treng streaming

Det løner seg å knyte ChatGPT API-en til Delphi FMX/VCL på ein ryddig måte, særleg der UI-responsivitet, driftssikkerleik og debugeigenskapar er viktige: admin‑verktøy, prosessnære skrivebords‑klientar eller supportverktøy i digitale bedriftsløysingar. Kodeutsnittet som er vist, er med vilje pragmatisk: SSE‑streaming utan spesialbibliotek, retry berre for faktiske nettverksfeil, JSON parsast defensivt.

Begrensningar: Om de treng strenge krav til etterleving, sentral Prompt-Governance, fleirmandantstøtte eller detaljerte revisjonsspor, er oftast „ein klient på skrivebordet“ ikkje nok. Då høyrer tilkoplinga vanlegvis på ein kontrollert server (t.d. eigen REST-Service), som sentralt handhevar policyar, logging og tilgangsstyring. For mange Delphi-installasjonar er klienten vist her likevel eit robust utgangspunkt som kan trinnvis overførast til ei ryddigare overordna arkitektur.

I fagleg samanheng spelar også Openai API i Delphi og Delphi HTTP-klient timeout/retry ei viktig rolle når integrasjonar, dataflyt og vidareutvikling må fungere ryddig saman.

Diskuter prosjekt eller moderniseringsprosjekt med Net-Base.

Nächster Schritt

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

Vi støttar ikkje berre ved enkeltspørsmål, men òg når korte kildekodesnuttar, legacy-tema eller portalidéar skal utviklast til eit robust bedriftsprosjekt.

  • Eksisterande tilstand, målbiletet og tekniske risikoar blir vurderast samla.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
  • Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

Del innlegg

Del dette innlegget direkte

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

E-post

Instagram opnar i ein ny fane. Lenkje og kort tekst blir kopiert til utklippstavla på førehand.