Net-Base Magasin

15.07.2026

ChatGPT API med Delphi i FMX/VCL: Robust anslutning med streaming, retry och korrekt JSON-parsning

Så ansluter du ChatGPT API robust till Delphi FMX/VCL: HTTP-klient med timeouts och återförsök, SSE-streaming utan UI-frysningar, samt robust JSON-parsning för verktygsanrop och felhantering.

15.07.2026

Från magasinets tema till projektpraxis

Passande tjänste- och tekniksidor för inlägget

Varför „ChatGPT API med Delphi FMX/VCL“ i praktiken inte bara är en POST

Den som vill ansluta ChatGPT API med Delphi FMX/VCL hamnar snabbt vid en enkel HTTP-POST. I verkliga businessprogrammiljöer fallerar detta dock på tre punkter: (1) Timeouts och omförsök måste vara deterministiska, annars upplever användare ett „fruset“ UI, (2) Streaming (Server-Sent Events, kort SSE) är ofta lämpligt för god UX, men blir snabbt felbenäget i Delphi-trådningen, och (3) JSON är inte bara „ett objekt“: felmeddelanden, kvotproblem, tomma fält eller lätt förändrade svarformat måste hanteras robust.

Följande kodsnutt visar en ansats som fungerar lika i FMX och VCL: en egen, testbar klient som kan arbeta antingen icke-streaming eller streaming, marshalar UI-uppdateringar på ett rent sätt (det vill säga utförs via synkronisering till huvudtråden) och loggar talande vid fel. Dessutom är den byggd för att passa in i befintliga lagerstrukturer (t.ex. „API-Client“ i integrationslagret, UI hålls tunt).

Arkitekturskiss: Koppla loss UI, håll klienten testbar

I Delphi-projekt med lång historik hittar man ofta „HTTP i ButtonClick“. Det fungerar fram till första incidenten. Rekommenderat är en liten klient med:

  • Konfiguration: Base-URL, API-Key, Modell, Timeouts.
  • Transportskikt: HTTP-Request/Response, Retry, Timeout, Proxy/SSL-alternativ (beroende på drift).
  • Parser: JSON-avkodning, felobjekt, resultatextraktion.
  • UI-hooks: callback för token-/text-streaming, men utan hårt beroende av VCL/FMX-kontroller.

Så kan integrationen i individuell företagsprogramvara drivas rent: Klienten kan återanvändas i services, desktopklienter, adminverktyg eller test-harnessar.

Kodsnutt: Delphi-klient med SSE-streaming, timeout/omförsök och robust JSON

Koden använder THTTPClient (System.Net.HttpClient) och parserar medvetet endast minimalt med System.JSON. För SSE läses det radvis och man reagerar på „data: …“. Detta är inte en „WebSocket“, utan en HTTP-responsström som kontinuerligt levererar textlinjer. Viktigt: Vi läser i en bakgrundstråd och marshalar UI-uppdateringar till huvudtrå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: här API-nyckeln som ‚Authorization: Bearer …‘.
// I företagsmiljöer, se dessutom till att nycklar inte hamnar i loggar.
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 uppbyggnad 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
// Vanligt format: { ‚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(‚Oväntat JSON-svar (inte ett objekt).‘, 0, AJsonText);

Root := J as TJSONObject;
Choices := Root.GetValue<TJSONArray>(‚choices‘);
if (Choices = nil) or (Choices.Count = 0) then
raise EChatApiError.Create(‚Oväntat JSON-svar: „choices“ saknas/är tom.‘, 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(‚Oväntat JSON-svar: „message“ saknas.‘, 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
// Nätverks-/TLS-/timeoutfel: 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-fel ‚ + 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
// Starta streaming asynkront medvetet så att FMX/VCL inte blockeras.
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
// Vid streamingfel är innehållet ofta ändå JSON.
TThread.Queue(nil,
procedure
begin
AOnEvent(‚Start av streaming misslyckades (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: rader 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;

// Tolka 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<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.

Vad angreppssättet är bra för

Koden löser tre typiska problemklasser som snabbt blir kostsamma i VCL/FMX:

  • Streaming utan UI-hängningar: HTTP-strömmen läses i bakgrunden; UI-uppdateringar körs via TThread.Queue (asynkront i huvudtråden). Det är det robusta standardförfarandet i FMX och VCL.
  • Omförsök vid nätfel: Vid ENetHTTPClientException görs ett omförsök med backoff. Det är medvetet enkelt och kan senare utökas med statuskoder (429/5xx).
  • Robust JSON-parsning: I stället för att blint casta fält kontrolleras de stegvis. Det minskar „Invalid type cast“-fel vid specialfall i svaren.

Randvillkor, fallgropar och varianter

  • SSE är inte ett „vanligt JSON“: Vid streaming kommer en följd av events, inte ett enstaka JSON-svar. Därför är radvis läsning och igenkänning av [DONE] centralt.
  • THTTPClient och proxies/SSL: I administrationsnätverk är TLS-inspektion och proxykrav verklighet. Planera för THTTPClient.ProxySettings och eventuellt certifikatfrågor. Debugging: logga alltid statuskod/headers (utan API-Key).
  • Timeout-strategi: ResponseTimeout är känsligt vid streaming: Om du väljer för kort tid avskär klienten långa svar. I UI-verktyg är en längre timeout och en „Avbryt“-knapp ofta mer lämpligt än „kort och hårt“.
  • Trådavbrott: Utdraget visar ingen cancel-token. För produktiva verktyg är en avbrottsmekanism värdefull (t.ex. flagga + Http.CancelAll i nyare Delphi-versioner eller genom kontrollerat stream-avbrott).
  • Modell- och API-utveckling: Strukturen i svaren kan skilja sig åt. Håll parsern defensiv och centraliserad, inte utspridd i formulärkoden.

Debugging i befintliga Delphi-klienter: Vad du verkligen bör logga

I integrationsprojekt beror den första driftsättningen sällan på JSON, utan på omgivningsdetaljer. I en teknisk logg (fil, eventlogg, central logger) bör följande finnas i praktiken:

  • Request-ID (själv tilldelad), tidsstämpel, mål-URL (utan hemliga querysträngar).
  • HTTP-statuskod, Content-Type, svarslängd, körtid.
  • En förkortad Response-Body vid fel (t.ex. max. 4–8 KB), för att kunna identifiera quota-/policyfel.
  • Tydlig märkning av omförsöksförsök: Attempt, Delay, Exception-klass.

API-Key hör aldrig hemma i loggen. Om du loggar request-body så endast i diagnostikbyggen och med maskering, eftersom prompts kan innehålla personuppgifter eller affärskänsligt innehåll.

Placering för legacy-situationer: VCL, FMX och Layer-3-arkitektur

Många Delphi-applikationer körs i en klassisk 3-lagerslogik („Layer-3 Architektur“: UI, affärslogik, data/integration). För ChatGPT-anslutning är det användbart: Den visade klienten hör till integrationslagret; affärslogiken beslutar, vad som frågas; UI visar bara historik och status. På så sätt undviker du att ett senare byte (annan leverantör, On-Prem-Proxy, nya endpunkter) river sönder formulären.

Även för Delphi-modernisering är detta en bra utgångspunkt: Först en stabil klient, sedan UI-förbättringar (streaming, avbryt, historik), och först därefter „mer intelligenta“ funktioner som strukturerade svar eller verktygsanrop.

Slutsats: Stabil grund, men inte varje applikation behöver streaming

Det lönar sig särskilt att ansluta ChatGPT API med Delphi FMX/VCL på ett ordnat sätt där UI-responsivitet, driftsäkerhet och felsökningsmöjligheter är viktiga: adminverktyg, processnära desktopklienter eller supportverktyg i digitala företagslösningar. Det visade kodsnutten är medvetet pragmatisk: SSE-streaming utan specialbibliotek, omförsök endast för verkliga nätverksfel, JSON parsas defensivt.

Begränsningar: Om ni har strikta compliance-krav, central prompt-governance, multitenans eller detaljerade auditspår räcker „en klient på desktop“ oftast inte. Då bör anslutningen typiskt placeras i en kontrollerad server (t.ex. en egen REST-tjänst) som centralt implementerar policys, loggning och åtkomststyrning. För många Delphi-installationer är den här visade klienten dock en robust utgångspunkt som kan migreras stegvis till en renare övergripande arkitektur.

I den tekniska kontexten spelar också Openai API i Delphi och Delphi HTTP-klienters timeout- och omförsökslogik en viktig roll när integrationer, dataflöden och vidareutveckling måste fungera väl ihop.

Diskutera projekt eller moderniseringsinitiativ 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öder inte bara vid enstaka frågor, utan även när kodsfragment, legacy-frågor eller portalidéer ska utvecklas till ett robust företagsprojekt.

  • Nuläge, målbild och tekniska risker bedöms tillsammans.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spätfolgen verschoben.
  • Sie sehen früh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

Dela inlägg

Dela det här inlägget direkt

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 öppnas i en ny flik. Länken och korttexten kopieras till urklipp först.