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.

neste steg

Når temaet blir eit reelt prosjekt, bør arkitektur, eksisterande system og drift tidleg saman vurderast.

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, datatilgang, portalar og utrulling blir ikkje utsett til seinare fasar.
  • De ser tidleg kva veg som er økonomisk og driftsmessig berekraftig.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e-post er straks tilgjengelege. For Instagram klargjer vi lenke og kort tekst med det same.

E-post

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