Gemini Test
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
program GeminiTest;
|
||||||
|
|
||||||
|
uses
|
||||||
|
System.StartUpCopy,
|
||||||
|
FMX.Forms,
|
||||||
|
MainUnit in 'MainUnit.pas' {Form1},
|
||||||
|
Myc.Api.Gemini in 'Myc.Api.Gemini.pas',
|
||||||
|
Myc.System.Quota in 'Myc.System.Quota.pas';
|
||||||
|
|
||||||
|
{$R *.res}
|
||||||
|
|
||||||
|
begin
|
||||||
|
Application.Initialize;
|
||||||
|
Application.CreateForm(TForm1, Form1);
|
||||||
|
Application.Run;
|
||||||
|
end.
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,187 @@
|
|||||||
|
object Form1: TForm1
|
||||||
|
Left = 0
|
||||||
|
Top = 0
|
||||||
|
Caption = 'Form1'
|
||||||
|
ClientHeight = 708
|
||||||
|
ClientWidth = 906
|
||||||
|
FormFactor.Width = 320
|
||||||
|
FormFactor.Height = 480
|
||||||
|
FormFactor.Devices = [Desktop]
|
||||||
|
OnCreate = FormCreate
|
||||||
|
DesignerMasterStyle = 0
|
||||||
|
object AskButton: TButton
|
||||||
|
Anchors = [akRight, akBottom]
|
||||||
|
Position.X = 800.000000000000000000
|
||||||
|
Position.Y = 577.000000000000000000
|
||||||
|
TabOrder = 1
|
||||||
|
Text = 'Ask'
|
||||||
|
OnClick = AskButtonClick
|
||||||
|
end
|
||||||
|
object Edit1: TEdit
|
||||||
|
Touch.InteractiveGestures = [LongTap, DoubleTap]
|
||||||
|
Anchors = [akLeft, akBottom]
|
||||||
|
TabOrder = 2
|
||||||
|
Text = 'AIzaSyBiWN55O-lI36EXcBeqf_cne4jAzxNsfbg'
|
||||||
|
Position.X = 8.000000000000000000
|
||||||
|
Position.Y = 678.000000000000000000
|
||||||
|
Size.Width = 145.000000000000000000
|
||||||
|
Size.Height = 22.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
OnExit = Edit1Exit
|
||||||
|
end
|
||||||
|
object ModelsComboBox: TComboBox
|
||||||
|
Anchors = [akLeft, akBottom]
|
||||||
|
Position.X = 8.000000000000000000
|
||||||
|
Position.Y = 577.000000000000000000
|
||||||
|
Size.Width = 193.000000000000000000
|
||||||
|
Size.Height = 22.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
TabOrder = 3
|
||||||
|
end
|
||||||
|
object AskMemo: TMemo
|
||||||
|
Touch.InteractiveGestures = [Pan, LongTap, DoubleTap]
|
||||||
|
DataDetectorTypes = []
|
||||||
|
Anchors = [akLeft, akRight, akBottom]
|
||||||
|
Position.X = 224.000000000000000000
|
||||||
|
Position.Y = 577.000000000000000000
|
||||||
|
Size.Width = 568.000000000000000000
|
||||||
|
Size.Height = 123.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
TabOrder = 5
|
||||||
|
Viewport.Width = 564.000000000000000000
|
||||||
|
Viewport.Height = 119.000000000000000000
|
||||||
|
end
|
||||||
|
object ResetChatButton: TButton
|
||||||
|
Anchors = [akRight, akBottom]
|
||||||
|
Position.X = 800.000000000000000000
|
||||||
|
Position.Y = 645.000000000000000000
|
||||||
|
TabOrder = 6
|
||||||
|
Text = 'Reset'
|
||||||
|
OnClick = ResetChatButtonClick
|
||||||
|
end
|
||||||
|
object TabControl1: TTabControl
|
||||||
|
Anchors = [akLeft, akTop, akRight, akBottom]
|
||||||
|
Size.Width = 906.000000000000000000
|
||||||
|
Size.Height = 561.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
TabIndex = 0
|
||||||
|
TabOrder = 7
|
||||||
|
TabPosition = PlatformDefault
|
||||||
|
Sizes = (
|
||||||
|
906s
|
||||||
|
535s
|
||||||
|
906s
|
||||||
|
535s
|
||||||
|
906s
|
||||||
|
535s)
|
||||||
|
object ChatTabItem: TTabItem
|
||||||
|
CustomIcon = <
|
||||||
|
item
|
||||||
|
end>
|
||||||
|
IsSelected = True
|
||||||
|
Size.Width = 45.000000000000000000
|
||||||
|
Size.Height = 26.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
StyleLookup = ''
|
||||||
|
TabOrder = 0
|
||||||
|
Text = 'Chat'
|
||||||
|
object ChatMemo: TMemo
|
||||||
|
Touch.InteractiveGestures = [Pan, LongTap, DoubleTap]
|
||||||
|
DataDetectorTypes = []
|
||||||
|
Align = Client
|
||||||
|
Size.Width = 906.000000000000000000
|
||||||
|
Size.Height = 535.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
TabOrder = 1
|
||||||
|
Viewport.Width = 902.000000000000000000
|
||||||
|
Viewport.Height = 531.000000000000000000
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object CacheTabItem: TTabItem
|
||||||
|
CustomIcon = <
|
||||||
|
item
|
||||||
|
end>
|
||||||
|
IsSelected = False
|
||||||
|
Size.Width = 53.000000000000000000
|
||||||
|
Size.Height = 26.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
StyleLookup = ''
|
||||||
|
TabOrder = 0
|
||||||
|
Text = 'Cache'
|
||||||
|
object CacheMemo: TMemo
|
||||||
|
Touch.InteractiveGestures = [Pan, LongTap, DoubleTap]
|
||||||
|
DataDetectorTypes = []
|
||||||
|
Align = Client
|
||||||
|
Size.Width = 906.000000000000000000
|
||||||
|
Size.Height = 535.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
TabOrder = 1
|
||||||
|
Viewport.Width = 902.000000000000000000
|
||||||
|
Viewport.Height = 531.000000000000000000
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object SysInstrTabItem: TTabItem
|
||||||
|
CustomIcon = <
|
||||||
|
item
|
||||||
|
end>
|
||||||
|
IsSelected = False
|
||||||
|
Size.Width = 61.000000000000000000
|
||||||
|
Size.Height = 26.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
StyleLookup = ''
|
||||||
|
TabOrder = 0
|
||||||
|
Text = 'SysInstr'
|
||||||
|
object SystemInstructionMemo: TMemo
|
||||||
|
Touch.InteractiveGestures = [Pan, LongTap, DoubleTap]
|
||||||
|
DataDetectorTypes = []
|
||||||
|
TextSettings.WordWrap = True
|
||||||
|
Align = Client
|
||||||
|
Size.Width = 906.000000000000000000
|
||||||
|
Size.Height = 535.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
TabOrder = 1
|
||||||
|
Viewport.Width = 902.000000000000000000
|
||||||
|
Viewport.Height = 531.000000000000000000
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
object UploadCacheButton: TButton
|
||||||
|
Anchors = [akRight, akBottom]
|
||||||
|
Position.X = 800.000000000000000000
|
||||||
|
Position.Y = 675.000000000000000000
|
||||||
|
TabOrder = 0
|
||||||
|
Text = 'Upload'
|
||||||
|
OnClick = UploadCacheButtonClick
|
||||||
|
end
|
||||||
|
object ThinkingCheckBox: TCheckBox
|
||||||
|
Anchors = [akRight, akBottom]
|
||||||
|
Position.X = 800.000000000000000000
|
||||||
|
Position.Y = 607.000000000000000000
|
||||||
|
Size.Width = 88.000000000000000000
|
||||||
|
Size.Height = 19.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
TabOrder = 8
|
||||||
|
Text = 'Thinking'
|
||||||
|
end
|
||||||
|
object GlobalQuoteLabel: TLabel
|
||||||
|
Anchors = [akLeft, akBottom]
|
||||||
|
Position.X = 8.000000000000000000
|
||||||
|
Position.Y = 608.000000000000000000
|
||||||
|
Size.Width = 193.000000000000000000
|
||||||
|
Size.Height = 17.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
Text = 'Global Quota: 0'
|
||||||
|
TabOrder = 9
|
||||||
|
end
|
||||||
|
object QuotaLabel: TLabel
|
||||||
|
Anchors = [akRight, akBottom]
|
||||||
|
Position.X = 704.000000000000000000
|
||||||
|
Position.Y = 537.000000000000000000
|
||||||
|
Size.Width = 194.000000000000000000
|
||||||
|
Size.Height = 16.000000000000000000
|
||||||
|
Size.PlatformDefault = False
|
||||||
|
TextSettings.HorzAlign = Trailing
|
||||||
|
Text = 'in/out'
|
||||||
|
TabOrder = 4
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
unit MainUnit;
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
System.SysUtils,
|
||||||
|
System.Types,
|
||||||
|
System.UITypes,
|
||||||
|
System.Classes,
|
||||||
|
System.Variants,
|
||||||
|
FMX.Types,
|
||||||
|
FMX.Controls,
|
||||||
|
FMX.Forms,
|
||||||
|
FMX.Graphics,
|
||||||
|
FMX.Dialogs,
|
||||||
|
FMX.Memo.Types,
|
||||||
|
FMX.ScrollBox,
|
||||||
|
FMX.Memo,
|
||||||
|
FMX.Controls.Presentation,
|
||||||
|
FMX.StdCtrls,
|
||||||
|
FMX.Edit,
|
||||||
|
FMX.ListBox,
|
||||||
|
Myc.Api.Gemini,
|
||||||
|
FMX.TabControl;
|
||||||
|
|
||||||
|
type
|
||||||
|
TForm1 = class(TForm)
|
||||||
|
AskButton: TButton;
|
||||||
|
Edit1: TEdit;
|
||||||
|
ModelsComboBox: TComboBox;
|
||||||
|
QuotaLabel: TLabel;
|
||||||
|
AskMemo: TMemo;
|
||||||
|
ResetChatButton: TButton;
|
||||||
|
TabControl1: TTabControl;
|
||||||
|
ChatTabItem: TTabItem;
|
||||||
|
CacheTabItem: TTabItem;
|
||||||
|
SysInstrTabItem: TTabItem;
|
||||||
|
ChatMemo: TMemo;
|
||||||
|
CacheMemo: TMemo;
|
||||||
|
SystemInstructionMemo: TMemo;
|
||||||
|
UploadCacheButton: TButton;
|
||||||
|
ThinkingCheckBox: TCheckBox;
|
||||||
|
GlobalQuoteLabel: TLabel;
|
||||||
|
procedure FormCreate(Sender: TObject);
|
||||||
|
procedure AskButtonClick(Sender: TObject);
|
||||||
|
procedure Edit1Exit(Sender: TObject);
|
||||||
|
procedure ResetChatButtonClick(Sender: TObject);
|
||||||
|
procedure UploadCacheButtonClick(Sender: TObject);
|
||||||
|
private
|
||||||
|
FChat: IGeminiChat;
|
||||||
|
// Holds the client configured with the active cache
|
||||||
|
FCachedClient: IGeminiClient;
|
||||||
|
procedure InitChat;
|
||||||
|
procedure UpdateQuotaDisplay;
|
||||||
|
public
|
||||||
|
procedure GetAvailableModels;
|
||||||
|
end;
|
||||||
|
|
||||||
|
var
|
||||||
|
Form1: TForm1;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
uses
|
||||||
|
System.Threading,
|
||||||
|
Myc.System.Quota; // Einbindung für den globalen Zähler
|
||||||
|
|
||||||
|
{$R *.fmx}
|
||||||
|
|
||||||
|
const
|
||||||
|
FallbackModel = 'gemini-3-flash-preview';
|
||||||
|
|
||||||
|
procedure TForm1.FormCreate(Sender: TObject);
|
||||||
|
begin
|
||||||
|
// Load default instructions if file exists
|
||||||
|
if FileExists('T:\Myc\KI\gemini.md') then
|
||||||
|
SystemInstructionMemo.Lines.LoadFromFile('T:\Myc\KI\gemini.md', TEncoding.UTF8);
|
||||||
|
|
||||||
|
// Initialen Global-Stand anzeigen
|
||||||
|
UpdateQuotaDisplay;
|
||||||
|
|
||||||
|
GetAvailableModels;
|
||||||
|
InitChat;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TForm1.InitChat;
|
||||||
|
begin
|
||||||
|
if Assigned(FCachedClient) then
|
||||||
|
begin
|
||||||
|
FChat := FCachedClient.StartChat;
|
||||||
|
ChatMemo.Lines.Add('--- New Chat Session (Cached Content Active) ---');
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
FChat := nil; // Lazy init in AskButton
|
||||||
|
ChatMemo.Lines.Add('--- New Chat Session ---');
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TForm1.ResetChatButtonClick(Sender: TObject);
|
||||||
|
begin
|
||||||
|
InitChat;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TForm1.UpdateQuotaDisplay;
|
||||||
|
begin
|
||||||
|
GlobalQuoteLabel.Text := 'Total: ' + TGlobalQuota.GetTotal.ToString + ', Today: ' + TGlobalQuota.GetDailyTotal.ToString;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TForm1.AskButtonClick(Sender: TObject);
|
||||||
|
var
|
||||||
|
apiKey: string;
|
||||||
|
modelName: string;
|
||||||
|
prompt: string;
|
||||||
|
begin
|
||||||
|
apiKey := Edit1.Text;
|
||||||
|
prompt := AskMemo.Text;
|
||||||
|
|
||||||
|
if ModelsComboBox.ItemIndex > -1 then
|
||||||
|
modelName := ModelsComboBox.Items[ModelsComboBox.ItemIndex]
|
||||||
|
else
|
||||||
|
modelName := FallbackModel;
|
||||||
|
|
||||||
|
// --- LAZY INIT ---
|
||||||
|
if FChat = nil then
|
||||||
|
begin
|
||||||
|
var client: IGeminiClient := TGeminiClient.Create(apiKey, modelName);
|
||||||
|
|
||||||
|
// 1. System Instructions (Ohne Hack, purer Text)
|
||||||
|
if not SystemInstructionMemo.Text.Trim.IsEmpty then
|
||||||
|
client.SystemInstruction := SystemInstructionMemo.Text;
|
||||||
|
|
||||||
|
// 2. Native Thinking aktivieren
|
||||||
|
// Die API-Unit entscheidet, ob das Modell es unterstützt.
|
||||||
|
// Falls ja, wird der 'thinking' Parameter gesendet.
|
||||||
|
client.EnableThinking := ThinkingCheckBox.IsChecked;
|
||||||
|
|
||||||
|
FChat := client.StartChat;
|
||||||
|
end;
|
||||||
|
|
||||||
|
AskButton.Enabled := False;
|
||||||
|
ChatMemo.Lines.Add(sLineBreak + 'User: "' + prompt + '"');
|
||||||
|
ChatMemo.Lines.Add('Gemini: ');
|
||||||
|
AskMemo.Text := '';
|
||||||
|
|
||||||
|
TTask.Run(
|
||||||
|
procedure
|
||||||
|
var
|
||||||
|
CurrentChat: IGeminiChat;
|
||||||
|
resultData: TGeminiResult;
|
||||||
|
begin
|
||||||
|
CurrentChat := FChat;
|
||||||
|
if CurrentChat = nil then
|
||||||
|
exit;
|
||||||
|
|
||||||
|
// --- STREAMING ---
|
||||||
|
resultData :=
|
||||||
|
CurrentChat.SendMessageStream(
|
||||||
|
prompt,
|
||||||
|
procedure(const TextChunk: string; IsThought: Boolean)
|
||||||
|
begin
|
||||||
|
TThread.Queue(
|
||||||
|
nil,
|
||||||
|
procedure
|
||||||
|
begin
|
||||||
|
if IsThought then
|
||||||
|
begin
|
||||||
|
// Visuelle Kennzeichnung für Gedanken
|
||||||
|
// (Hier einfach in eckigen Klammern, besser wäre Farbe)
|
||||||
|
ChatMemo.Text := ChatMemo.Text + ' [Think: ' + TextChunk + '] ';
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
// Normale Antwort
|
||||||
|
ChatMemo.Text := ChatMemo.Text + TextChunk;
|
||||||
|
end;
|
||||||
|
|
||||||
|
ChatMemo.GoToTextEnd;
|
||||||
|
end
|
||||||
|
);
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
|
// Abschluss im Main Thread
|
||||||
|
TThread.Queue(
|
||||||
|
nil,
|
||||||
|
procedure
|
||||||
|
begin
|
||||||
|
if resultData.Text.StartsWith('Error') then
|
||||||
|
ChatMemo.Lines.Add(resultData.Text);
|
||||||
|
|
||||||
|
// Update Labels (Session & Global Registry)
|
||||||
|
QuotaLabel.Text := Format('Input: %d | Output: %d', [resultData.Usage.PromptTokens, resultData.Usage.CandidatesTokens]);
|
||||||
|
UpdateQuotaDisplay;
|
||||||
|
|
||||||
|
AskButton.Enabled := True;
|
||||||
|
end
|
||||||
|
);
|
||||||
|
end
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TForm1.Edit1Exit(Sender: TObject);
|
||||||
|
begin
|
||||||
|
GetAvailableModels;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TForm1.GetAvailableModels;
|
||||||
|
var
|
||||||
|
apiKey: string;
|
||||||
|
begin
|
||||||
|
apiKey := Edit1.Text.Trim;
|
||||||
|
if apiKey.IsEmpty then
|
||||||
|
exit;
|
||||||
|
|
||||||
|
ChatMemo.Lines.Add('Fetching available models...');
|
||||||
|
|
||||||
|
TTask.Run(
|
||||||
|
procedure
|
||||||
|
var
|
||||||
|
client: IGeminiClient;
|
||||||
|
modelList: TArray<string>;
|
||||||
|
begin
|
||||||
|
client := TGeminiClient.Create(apiKey);
|
||||||
|
modelList := client.ListModels;
|
||||||
|
|
||||||
|
TThread.Queue(
|
||||||
|
nil,
|
||||||
|
procedure
|
||||||
|
var
|
||||||
|
mName: string;
|
||||||
|
begin
|
||||||
|
if Length(modelList) = 0 then
|
||||||
|
begin
|
||||||
|
ChatMemo.Lines.Add('No models found.');
|
||||||
|
exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
ModelsComboBox.BeginUpdate;
|
||||||
|
try
|
||||||
|
ModelsComboBox.Items.Clear;
|
||||||
|
|
||||||
|
for mName in modelList do
|
||||||
|
ModelsComboBox.Items.Add(mName);
|
||||||
|
|
||||||
|
// Versuche Fallback zu finden (ggf. ohne Emoji suffix match)
|
||||||
|
// Vereinfacht: Wähle ersten Index
|
||||||
|
if ModelsComboBox.Items.Count > 0 then
|
||||||
|
begin
|
||||||
|
ModelsComboBox.ItemIndex := ModelsComboBox.Items.IndexOf(FallbackModel);
|
||||||
|
if ModelsComboBox.ItemIndex < 0 then
|
||||||
|
ModelsComboBox.ItemIndex := 0;
|
||||||
|
end;
|
||||||
|
|
||||||
|
ChatMemo.Lines.Add(Format('Loaded %d models.', [Length(modelList)]));
|
||||||
|
finally
|
||||||
|
ModelsComboBox.EndUpdate;
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
);
|
||||||
|
end
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TForm1.UploadCacheButtonClick(Sender: TObject);
|
||||||
|
var
|
||||||
|
apiKey: string;
|
||||||
|
content: string;
|
||||||
|
sysInstr: string;
|
||||||
|
modelName: string;
|
||||||
|
begin
|
||||||
|
apiKey := Edit1.Text;
|
||||||
|
content := CacheMemo.Text;
|
||||||
|
|
||||||
|
// Keine künstlichen Prompts mehr, nur die reinen Instruktionen
|
||||||
|
sysInstr := SystemInstructionMemo.Text;
|
||||||
|
|
||||||
|
if content.Trim.IsEmpty then
|
||||||
|
begin
|
||||||
|
ChatMemo.Lines.Add('Error: Cache content is empty.');
|
||||||
|
exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Remove Emoji suffix if present from ComboBox for API call
|
||||||
|
if ModelsComboBox.ItemIndex > -1 then
|
||||||
|
modelName := ModelsComboBox.Items[ModelsComboBox.ItemIndex].Split([' '])[0]
|
||||||
|
else
|
||||||
|
modelName := FallbackModel;
|
||||||
|
|
||||||
|
UploadCacheButton.Enabled := False;
|
||||||
|
ChatMemo.Lines.Add('Creating Cache (this may take a moment)...');
|
||||||
|
|
||||||
|
TTask.Run(
|
||||||
|
procedure
|
||||||
|
var
|
||||||
|
client: IGeminiClient;
|
||||||
|
cacheName: string;
|
||||||
|
errMsg: string;
|
||||||
|
begin
|
||||||
|
try
|
||||||
|
// 1. Client erstellen
|
||||||
|
client := TGeminiClient.Create(apiKey, modelName);
|
||||||
|
|
||||||
|
// 2. Cache erstellen (Content + SysInstr)
|
||||||
|
// Hinweis: Native Thinking ist ein Parameter der GENERIERUNG, nicht des Caches.
|
||||||
|
// Daher laden wir den Cache "neutral" hoch.
|
||||||
|
cacheName := client.CreateCache(content, sysInstr, 300);
|
||||||
|
|
||||||
|
// 3. Cache aktivieren
|
||||||
|
client.ActiveCacheName := cacheName;
|
||||||
|
|
||||||
|
// 4. Client speichern
|
||||||
|
// Wenn der User später "Ask" klickt, wird dort EnableThinking gesetzt
|
||||||
|
FCachedClient := client;
|
||||||
|
|
||||||
|
TThread.Queue(
|
||||||
|
nil,
|
||||||
|
procedure
|
||||||
|
begin
|
||||||
|
ChatMemo.Lines.Add('Success: Cache created!');
|
||||||
|
ChatMemo.Lines.Add('ID: ' + cacheName);
|
||||||
|
|
||||||
|
InitChat;
|
||||||
|
UploadCacheButton.Enabled := True;
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
errMsg := E.Message;
|
||||||
|
TThread.Queue(
|
||||||
|
nil,
|
||||||
|
procedure
|
||||||
|
begin
|
||||||
|
ChatMemo.Lines.Add('Cache Upload Failed:');
|
||||||
|
ChatMemo.Lines.Add(errMsg);
|
||||||
|
UploadCacheButton.Enabled := True;
|
||||||
|
end
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -0,0 +1,822 @@
|
|||||||
|
unit Myc.Api.Gemini;
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
System.SysUtils,
|
||||||
|
System.Classes,
|
||||||
|
System.Net.HttpClient,
|
||||||
|
System.Net.Mime,
|
||||||
|
System.JSON,
|
||||||
|
System.NetEncoding,
|
||||||
|
System.Generics.Collections,
|
||||||
|
System.SyncObjs,
|
||||||
|
Myc.System.Quota; // Einbindung der Global Quota Unit
|
||||||
|
|
||||||
|
type
|
||||||
|
// Represents usage statistics for a request
|
||||||
|
TGeminiUsage = record
|
||||||
|
PromptTokens: Integer;
|
||||||
|
CandidatesTokens: Integer;
|
||||||
|
TotalTokens: Integer;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Represents the result of a generation request
|
||||||
|
TGeminiResult = record
|
||||||
|
Text: string;
|
||||||
|
IsThought: Boolean; // True if this part contains reasoning/thoughts
|
||||||
|
Usage: TGeminiUsage;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TChatRole = (User, Model);
|
||||||
|
|
||||||
|
TChatItem = record
|
||||||
|
Role: TChatRole;
|
||||||
|
Text: string;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Callback for streaming: provides the new text delta and thought flag
|
||||||
|
TStreamCallback = reference to procedure(const TextDelta: string; IsThought: Boolean);
|
||||||
|
|
||||||
|
// Interface for a chat session (maintains history)
|
||||||
|
IGeminiChat = interface
|
||||||
|
['{5C2F64C8-4D18-47C0-9513-332715053673}']
|
||||||
|
function SendMessage(const Text: string): TGeminiResult;
|
||||||
|
function SendMessageStream(const Text: string; const OnDelta: TStreamCallback): TGeminiResult;
|
||||||
|
function GetHistory: TArray<TChatItem>;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Interface for the API Client
|
||||||
|
IGeminiClient = interface
|
||||||
|
['{E6721670-3498-4660-848D-82F3B1A2B5E2}']
|
||||||
|
// Standard Generation
|
||||||
|
function GenerateContent(const Prompt: string): TGeminiResult; overload;
|
||||||
|
function GenerateContent(const History: TArray<TChatItem>): TGeminiResult; overload;
|
||||||
|
|
||||||
|
// Streaming Generation
|
||||||
|
function GenerateContentStream(const Prompt: string; const OnDelta: TStreamCallback): TGeminiResult; overload;
|
||||||
|
function GenerateContentStream(const History: TArray<TChatItem>; const OnDelta: TStreamCallback): TGeminiResult; overload;
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
function ListModels: TArray<string>;
|
||||||
|
function StartChat: IGeminiChat;
|
||||||
|
|
||||||
|
// Context Caching
|
||||||
|
function CreateCache(const Content: string; const SysInstructions: string; const TTLSeconds: Integer = 300): string;
|
||||||
|
procedure SetActiveCache(const CacheName: string);
|
||||||
|
function GetActiveCache: string;
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
procedure SetSystemInstruction(const Value: string);
|
||||||
|
function GetSystemInstruction: string;
|
||||||
|
|
||||||
|
// Native Thinking / Reasoning
|
||||||
|
procedure SetEnableThinking(const Value: Boolean);
|
||||||
|
function GetEnableThinking: Boolean;
|
||||||
|
|
||||||
|
property ActiveCacheName: string read GetActiveCache write SetActiveCache;
|
||||||
|
property SystemInstruction: string read GetSystemInstruction write SetSystemInstruction;
|
||||||
|
property EnableThinking: Boolean read GetEnableThinking write SetEnableThinking;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Implementation of the API Client
|
||||||
|
TGeminiClient = class(TInterfacedObject, IGeminiClient)
|
||||||
|
private
|
||||||
|
class var
|
||||||
|
FTotalUsage: TGeminiUsage;
|
||||||
|
private
|
||||||
|
FApiKey: string;
|
||||||
|
FBaseUrl: string;
|
||||||
|
FStreamUrl: string;
|
||||||
|
FListUrl: string;
|
||||||
|
FModelName: string;
|
||||||
|
FActiveCacheName: string;
|
||||||
|
FSystemInstruction: string;
|
||||||
|
FEnableThinking: Boolean;
|
||||||
|
|
||||||
|
function ParseResponse(const JsonStr: string): TGeminiResult;
|
||||||
|
function ParseModels(const JsonStr: string): TArray<string>;
|
||||||
|
function BuildJsonBody(const History: TArray<TChatItem>): TJSONObject;
|
||||||
|
procedure ProcessStreamData(const Data: TBytes; var Buffer: string; const OnDelta: TStreamCallback; var TotalResult: TGeminiResult);
|
||||||
|
public
|
||||||
|
constructor Create(const AApiKey: string; const AModel: string = 'gemini-1.5-flash-001');
|
||||||
|
|
||||||
|
function GenerateContent(const Prompt: string): TGeminiResult; overload;
|
||||||
|
function GenerateContent(const History: TArray<TChatItem>): TGeminiResult; overload;
|
||||||
|
|
||||||
|
function GenerateContentStream(const Prompt: string; const OnDelta: TStreamCallback): TGeminiResult; overload;
|
||||||
|
function GenerateContentStream(const History: TArray<TChatItem>; const OnDelta: TStreamCallback): TGeminiResult; overload;
|
||||||
|
|
||||||
|
function ListModels: TArray<string>;
|
||||||
|
function StartChat: IGeminiChat;
|
||||||
|
|
||||||
|
function CreateCache(const Content: string; const SysInstructions: string; const TTLSeconds: Integer = 300): string;
|
||||||
|
procedure SetActiveCache(const CacheName: string);
|
||||||
|
function GetActiveCache: string;
|
||||||
|
|
||||||
|
procedure SetSystemInstruction(const Value: string);
|
||||||
|
function GetSystemInstruction: string;
|
||||||
|
|
||||||
|
procedure SetEnableThinking(const Value: Boolean);
|
||||||
|
function GetEnableThinking: Boolean;
|
||||||
|
|
||||||
|
// Static access to session-global usage (RAM only)
|
||||||
|
class property TotalUsage: TGeminiUsage read FTotalUsage;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
type
|
||||||
|
// Internal Chat Session Implementation
|
||||||
|
TChatSession = class(TInterfacedObject, IGeminiChat)
|
||||||
|
private
|
||||||
|
FClient: IGeminiClient;
|
||||||
|
FHistory: TList<TChatItem>;
|
||||||
|
public
|
||||||
|
constructor Create(const Client: IGeminiClient);
|
||||||
|
destructor Destroy; override;
|
||||||
|
function SendMessage(const Text: string): TGeminiResult;
|
||||||
|
function SendMessageStream(const Text: string; const OnDelta: TStreamCallback): TGeminiResult;
|
||||||
|
function GetHistory: TArray<TChatItem>;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ TGeminiClient }
|
||||||
|
|
||||||
|
constructor TGeminiClient.Create(const AApiKey: string; const AModel: string);
|
||||||
|
var
|
||||||
|
LModel: string;
|
||||||
|
encodedKey: string;
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FApiKey := AApiKey.Trim;
|
||||||
|
LModel := AModel.Trim;
|
||||||
|
|
||||||
|
if LModel.IsEmpty then
|
||||||
|
LModel := 'gemini-1.5-flash-001';
|
||||||
|
|
||||||
|
FModelName := LModel;
|
||||||
|
if not FModelName.StartsWith('models/') then
|
||||||
|
FModelName := 'models/' + FModelName;
|
||||||
|
|
||||||
|
encodedKey := TNetEncoding.URL.EncodeQuery(FApiKey);
|
||||||
|
|
||||||
|
// Endpoint construction
|
||||||
|
FBaseUrl := Format('https://generativelanguage.googleapis.com/v1beta/%s:generateContent?key=%s', [FModelName, encodedKey]);
|
||||||
|
|
||||||
|
FStreamUrl := Format('https://generativelanguage.googleapis.com/v1beta/%s:streamGenerateContent?key=%s', [FModelName, encodedKey]);
|
||||||
|
|
||||||
|
FListUrl := Format('https://generativelanguage.googleapis.com/v1beta/models?key=%s', [encodedKey]);
|
||||||
|
|
||||||
|
FEnableThinking := False;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// --- Property Setters/Getters ---
|
||||||
|
|
||||||
|
procedure TGeminiClient.SetEnableThinking(const Value: Boolean);
|
||||||
|
begin
|
||||||
|
FEnableThinking := Value;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TGeminiClient.GetEnableThinking: Boolean;
|
||||||
|
begin
|
||||||
|
Result := FEnableThinking;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TGeminiClient.SetActiveCache(const CacheName: string);
|
||||||
|
begin
|
||||||
|
FActiveCacheName := CacheName;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TGeminiClient.GetActiveCache: string;
|
||||||
|
begin
|
||||||
|
Result := FActiveCacheName;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TGeminiClient.SetSystemInstruction(const Value: string);
|
||||||
|
begin
|
||||||
|
FSystemInstruction := Value;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TGeminiClient.GetSystemInstruction: string;
|
||||||
|
begin
|
||||||
|
Result := FSystemInstruction;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// --- Core Logic ---
|
||||||
|
|
||||||
|
function TGeminiClient.GenerateContent(const Prompt: string): TGeminiResult;
|
||||||
|
var
|
||||||
|
item: TChatItem;
|
||||||
|
begin
|
||||||
|
item.Role := TChatRole.User;
|
||||||
|
item.Text := Prompt;
|
||||||
|
Result := GenerateContent([item]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TGeminiClient.GenerateContent(const History: TArray<TChatItem>): TGeminiResult;
|
||||||
|
var
|
||||||
|
httpClient: THTTPClient;
|
||||||
|
requestBody: TJSONObject;
|
||||||
|
response: IHTTPResponse;
|
||||||
|
stringStream: TStringStream;
|
||||||
|
begin
|
||||||
|
Result.Text := '';
|
||||||
|
Result.Usage := Default(TGeminiUsage);
|
||||||
|
Result.IsThought := False;
|
||||||
|
|
||||||
|
httpClient := THTTPClient.Create;
|
||||||
|
requestBody := BuildJsonBody(History);
|
||||||
|
try
|
||||||
|
stringStream := TStringStream.Create(requestBody.ToString, TEncoding.UTF8);
|
||||||
|
try
|
||||||
|
httpClient.CustomHeaders['Content-Type'] := 'application/json';
|
||||||
|
httpClient.ConnectionTimeout := 10000;
|
||||||
|
httpClient.ResponseTimeout := 30000;
|
||||||
|
|
||||||
|
try
|
||||||
|
response := httpClient.Post(FBaseUrl, stringStream);
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
Result.Text := 'Error: Connection failed - ' + E.Message;
|
||||||
|
exit;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if (response.StatusCode = 200) then
|
||||||
|
begin
|
||||||
|
Result := ParseResponse(response.ContentAsString(TEncoding.UTF8));
|
||||||
|
|
||||||
|
// Update Usages
|
||||||
|
if (Result.Usage.TotalTokens > 0) then
|
||||||
|
begin
|
||||||
|
// RAM Counter
|
||||||
|
TInterlocked.Add(FTotalUsage.PromptTokens, Result.Usage.PromptTokens);
|
||||||
|
TInterlocked.Add(FTotalUsage.CandidatesTokens, Result.Usage.CandidatesTokens);
|
||||||
|
TInterlocked.Add(FTotalUsage.TotalTokens, Result.Usage.TotalTokens);
|
||||||
|
|
||||||
|
// Persistent System Counter
|
||||||
|
TGlobalQuota.AddTokens(Result.Usage.TotalTokens);
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
Result.Text :=
|
||||||
|
Format('Error: %d - %s (%s)', [response.StatusCode, response.StatusText, response.ContentAsString(TEncoding.UTF8)]);
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
stringStream.Free;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
httpClient.Free;
|
||||||
|
requestBody.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TGeminiClient.GenerateContentStream(const Prompt: string; const OnDelta: TStreamCallback): TGeminiResult;
|
||||||
|
var
|
||||||
|
item: TChatItem;
|
||||||
|
begin
|
||||||
|
item.Role := TChatRole.User;
|
||||||
|
item.Text := Prompt;
|
||||||
|
Result := GenerateContentStream([item], OnDelta);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TGeminiClient.GenerateContentStream(const History: TArray<TChatItem>; const OnDelta: TStreamCallback): TGeminiResult;
|
||||||
|
var
|
||||||
|
httpClient: THTTPClient;
|
||||||
|
requestBody: TJSONObject;
|
||||||
|
stringStream: TStringStream;
|
||||||
|
buffer: string;
|
||||||
|
|
||||||
|
// Captured local variable for the result state to avoid E2555
|
||||||
|
LFullResult: TGeminiResult;
|
||||||
|
begin
|
||||||
|
// Init local result container
|
||||||
|
LFullResult.Text := '';
|
||||||
|
LFullResult.Usage := Default(TGeminiUsage);
|
||||||
|
LFullResult.IsThought := False;
|
||||||
|
buffer := '';
|
||||||
|
|
||||||
|
httpClient := THTTPClient.Create;
|
||||||
|
requestBody := BuildJsonBody(History);
|
||||||
|
try
|
||||||
|
stringStream := TStringStream.Create(requestBody.ToString, TEncoding.UTF8);
|
||||||
|
try
|
||||||
|
httpClient.CustomHeaders['Content-Type'] := 'application/json';
|
||||||
|
httpClient.ConnectionTimeout := 10000;
|
||||||
|
httpClient.ResponseTimeout := 60000;
|
||||||
|
|
||||||
|
// Use ReceiveDataExCallback to access raw data chunks efficiently
|
||||||
|
httpClient.ReceiveDataExCallback :=
|
||||||
|
procedure(
|
||||||
|
const Sender: TObject;
|
||||||
|
AContentLength: Int64;
|
||||||
|
AReadCount: Int64;
|
||||||
|
AChunk: Pointer;
|
||||||
|
AChunkLength: Cardinal;
|
||||||
|
var AAbort: Boolean
|
||||||
|
)
|
||||||
|
var
|
||||||
|
LBytes: TBytes;
|
||||||
|
begin
|
||||||
|
if AChunkLength > 0 then
|
||||||
|
begin
|
||||||
|
SetLength(LBytes, AChunkLength);
|
||||||
|
Move(AChunk^, LBytes[0], AChunkLength);
|
||||||
|
|
||||||
|
ProcessStreamData(LBytes, buffer, OnDelta, LFullResult);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
try
|
||||||
|
httpClient.Post(FStreamUrl, stringStream);
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
begin
|
||||||
|
LFullResult.Text := LFullResult.Text + sLineBreak + '[Error: ' + E.Message + ']';
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Update Usages
|
||||||
|
if LFullResult.Usage.TotalTokens > 0 then
|
||||||
|
begin
|
||||||
|
// RAM Counter
|
||||||
|
TInterlocked.Add(FTotalUsage.PromptTokens, LFullResult.Usage.PromptTokens);
|
||||||
|
TInterlocked.Add(FTotalUsage.CandidatesTokens, LFullResult.Usage.CandidatesTokens);
|
||||||
|
TInterlocked.Add(FTotalUsage.TotalTokens, LFullResult.Usage.TotalTokens);
|
||||||
|
|
||||||
|
// Persistent System Counter
|
||||||
|
TGlobalQuota.AddTokens(LFullResult.Usage.TotalTokens);
|
||||||
|
end;
|
||||||
|
|
||||||
|
finally
|
||||||
|
stringStream.Free;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
httpClient.Free;
|
||||||
|
requestBody.Free;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Result := LFullResult;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// --- JSON Body Construction ---
|
||||||
|
|
||||||
|
function TGeminiClient.BuildJsonBody(const History: TArray<TChatItem>): TJSONObject;
|
||||||
|
var
|
||||||
|
contentsArray, partsArray: TJSONArray;
|
||||||
|
contentObj, partObj: TJSONObject;
|
||||||
|
sysObj, sysPart: TJSONObject;
|
||||||
|
sysPartsArr: TJSONArray;
|
||||||
|
item: TChatItem;
|
||||||
|
genConfig, thinkConfig: TJSONObject;
|
||||||
|
begin
|
||||||
|
Result := TJSONObject.Create;
|
||||||
|
|
||||||
|
// 1. Caching Strategy
|
||||||
|
if not FActiveCacheName.IsEmpty then
|
||||||
|
begin
|
||||||
|
Result.AddPair('cachedContent', FActiveCacheName);
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
// 2. Dynamic System Instructions (only if no cache active)
|
||||||
|
if not FSystemInstruction.IsEmpty then
|
||||||
|
begin
|
||||||
|
sysObj := TJSONObject.Create;
|
||||||
|
|
||||||
|
sysPartsArr := TJSONArray.Create;
|
||||||
|
sysPart := TJSONObject.Create;
|
||||||
|
sysPart.AddPair('text', FSystemInstruction);
|
||||||
|
sysPartsArr.Add(sysPart);
|
||||||
|
|
||||||
|
sysObj.AddPair('parts', sysPartsArr);
|
||||||
|
Result.AddPair('systemInstruction', sysObj);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// 3. Native Thinking Configuration
|
||||||
|
if FEnableThinking then
|
||||||
|
begin
|
||||||
|
thinkConfig := TJSONObject.Create;
|
||||||
|
thinkConfig.AddPair('includeThoughts', TJSONTrue.Create);
|
||||||
|
// 'thinkingLevel' omitted to use model default
|
||||||
|
|
||||||
|
genConfig := TJSONObject.Create;
|
||||||
|
genConfig.AddPair('thinkingConfig', thinkConfig);
|
||||||
|
|
||||||
|
Result.AddPair('generationConfig', genConfig);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// 4. Chat Contents
|
||||||
|
contentsArray := TJSONArray.Create;
|
||||||
|
|
||||||
|
for item in History do
|
||||||
|
begin
|
||||||
|
contentObj := TJSONObject.Create;
|
||||||
|
|
||||||
|
// Delphi 13 inline if
|
||||||
|
contentObj.AddPair(
|
||||||
|
'role',
|
||||||
|
if item.Role = TChatRole.User then 'user'
|
||||||
|
else 'model'
|
||||||
|
);
|
||||||
|
|
||||||
|
partObj := TJSONObject.Create;
|
||||||
|
partObj.AddPair('text', item.Text);
|
||||||
|
|
||||||
|
partsArray := TJSONArray.Create;
|
||||||
|
partsArray.Add(partObj);
|
||||||
|
|
||||||
|
contentObj.AddPair('parts', partsArray);
|
||||||
|
contentsArray.Add(contentObj);
|
||||||
|
end;
|
||||||
|
Result.AddPair('contents', contentsArray);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// --- Response Parsing (Streaming) ---
|
||||||
|
|
||||||
|
procedure TGeminiClient.ProcessStreamData(
|
||||||
|
const Data: TBytes;
|
||||||
|
var Buffer: string;
|
||||||
|
const OnDelta: TStreamCallback;
|
||||||
|
var TotalResult: TGeminiResult
|
||||||
|
);
|
||||||
|
var
|
||||||
|
chunkStr: string;
|
||||||
|
jsonStart, jsonEnd: Integer;
|
||||||
|
jsonSub: string;
|
||||||
|
jsonObj: TJSONObject;
|
||||||
|
candidates, parts: TJSONArray;
|
||||||
|
part, usageObj: TJSONObject;
|
||||||
|
deltaText: string;
|
||||||
|
isDeltaThought: Boolean;
|
||||||
|
braceCount, i: Integer;
|
||||||
|
foundEnd: Boolean;
|
||||||
|
begin
|
||||||
|
chunkStr := TEncoding.UTF8.GetString(Data);
|
||||||
|
Buffer := Buffer + chunkStr;
|
||||||
|
|
||||||
|
// Parse loop to extract complete JSON objects from buffer
|
||||||
|
while True do
|
||||||
|
begin
|
||||||
|
jsonStart := Buffer.IndexOf('{');
|
||||||
|
jsonEnd := -1;
|
||||||
|
if jsonStart < 0 then
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Brackets matching to find end of JSON object
|
||||||
|
braceCount := 0;
|
||||||
|
foundEnd := False;
|
||||||
|
for i := jsonStart to Buffer.Length - 1 do
|
||||||
|
begin
|
||||||
|
if Buffer.Chars[i] = '{' then
|
||||||
|
inc(braceCount)
|
||||||
|
else if Buffer.Chars[i] = '}' then
|
||||||
|
dec(braceCount);
|
||||||
|
|
||||||
|
if (braceCount = 0) then
|
||||||
|
begin
|
||||||
|
jsonEnd := i;
|
||||||
|
foundEnd := True;
|
||||||
|
break;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if not foundEnd then
|
||||||
|
break; // Object incomplete
|
||||||
|
|
||||||
|
jsonSub := Buffer.Substring(jsonStart, jsonEnd - jsonStart + 1);
|
||||||
|
Buffer := Buffer.Substring(jsonEnd + 1);
|
||||||
|
|
||||||
|
jsonObj := TJSONObject.ParseJSONValue(jsonSub) as TJSONObject;
|
||||||
|
if Assigned(jsonObj) then
|
||||||
|
try
|
||||||
|
deltaText := '';
|
||||||
|
isDeltaThought := False;
|
||||||
|
|
||||||
|
candidates := jsonObj.GetValue('candidates') as TJSONArray;
|
||||||
|
if (Assigned(candidates)) and (candidates.Count > 0) then
|
||||||
|
begin
|
||||||
|
var cand := candidates.Items[0] as TJSONObject;
|
||||||
|
var content := cand.GetValue('content') as TJSONObject;
|
||||||
|
if Assigned(content) then
|
||||||
|
begin
|
||||||
|
parts := content.GetValue('parts') as TJSONArray;
|
||||||
|
if (Assigned(parts)) and (parts.Count > 0) then
|
||||||
|
begin
|
||||||
|
part := parts.Items[0] as TJSONObject;
|
||||||
|
deltaText := part.GetValue<string>('text');
|
||||||
|
|
||||||
|
// Check for Native Thinking flag "thought": true
|
||||||
|
part.TryGetValue<Boolean>('thought', isDeltaThought);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
usageObj := jsonObj.GetValue('usageMetadata') as TJSONObject;
|
||||||
|
if Assigned(usageObj) then
|
||||||
|
begin
|
||||||
|
usageObj.TryGetValue<Integer>('promptTokenCount', TotalResult.Usage.PromptTokens);
|
||||||
|
usageObj.TryGetValue<Integer>('candidatesTokenCount', TotalResult.Usage.CandidatesTokens);
|
||||||
|
usageObj.TryGetValue<Integer>('totalTokenCount', TotalResult.Usage.TotalTokens);
|
||||||
|
end;
|
||||||
|
|
||||||
|
if not deltaText.IsEmpty then
|
||||||
|
begin
|
||||||
|
// Append to full text (including thoughts)
|
||||||
|
if not isDeltaThought then
|
||||||
|
TotalResult.Text := TotalResult.Text + deltaText;
|
||||||
|
|
||||||
|
// Fire callback
|
||||||
|
if Assigned(OnDelta) then
|
||||||
|
OnDelta(deltaText, isDeltaThought);
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
jsonObj.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// --- Response Parsing (Synchronous) ---
|
||||||
|
|
||||||
|
function TGeminiClient.ParseResponse(const JsonStr: string): TGeminiResult;
|
||||||
|
var
|
||||||
|
jsonObj: TJSONObject;
|
||||||
|
candidates, parts: TJSONArray;
|
||||||
|
candidate, content, part, usageObj: TJSONObject;
|
||||||
|
isThought: Boolean;
|
||||||
|
begin
|
||||||
|
Result.Text := '';
|
||||||
|
Result.Usage := Default(TGeminiUsage);
|
||||||
|
Result.IsThought := False;
|
||||||
|
|
||||||
|
jsonObj := TJSONObject.ParseJSONValue(JsonStr) as TJSONObject;
|
||||||
|
if not Assigned(jsonObj) then
|
||||||
|
exit;
|
||||||
|
|
||||||
|
try
|
||||||
|
candidates := jsonObj.GetValue('candidates') as TJSONArray;
|
||||||
|
if (Assigned(candidates)) and (candidates.Count > 0) then
|
||||||
|
begin
|
||||||
|
candidate := candidates.Items[0] as TJSONObject;
|
||||||
|
content := candidate.GetValue('content') as TJSONObject;
|
||||||
|
if Assigned(content) then
|
||||||
|
begin
|
||||||
|
parts := content.GetValue('parts') as TJSONArray;
|
||||||
|
if (Assigned(parts)) and (parts.Count > 0) then
|
||||||
|
begin
|
||||||
|
part := parts.Items[0] as TJSONObject;
|
||||||
|
Result.Text := part.GetValue<string>('text');
|
||||||
|
|
||||||
|
// Check for thought flag
|
||||||
|
if part.TryGetValue<Boolean>('thought', isThought) then
|
||||||
|
Result.IsThought := isThought;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
usageObj := jsonObj.GetValue('usageMetadata') as TJSONObject;
|
||||||
|
if Assigned(usageObj) then
|
||||||
|
begin
|
||||||
|
usageObj.TryGetValue<Integer>('promptTokenCount', Result.Usage.PromptTokens);
|
||||||
|
usageObj.TryGetValue<Integer>('candidatesTokenCount', Result.Usage.CandidatesTokens);
|
||||||
|
usageObj.TryGetValue<Integer>('totalTokenCount', Result.Usage.TotalTokens);
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
jsonObj.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// --- Model Listing ---
|
||||||
|
|
||||||
|
function TGeminiClient.ListModels: TArray<string>;
|
||||||
|
var
|
||||||
|
httpClient: THTTPClient;
|
||||||
|
response: IHTTPResponse;
|
||||||
|
begin
|
||||||
|
SetLength(Result, 0);
|
||||||
|
httpClient := THTTPClient.Create;
|
||||||
|
try
|
||||||
|
httpClient.ConnectionTimeout := 10000;
|
||||||
|
try
|
||||||
|
response := httpClient.Get(FListUrl);
|
||||||
|
if (response.StatusCode = 200) then
|
||||||
|
Result := ParseModels(response.ContentAsString(TEncoding.UTF8));
|
||||||
|
except
|
||||||
|
// Return empty array on error
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
httpClient.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TGeminiClient.ParseModels(const JsonStr: string): TArray<string>;
|
||||||
|
var
|
||||||
|
jsonObj: TJSONObject;
|
||||||
|
models, methods: TJSONArray;
|
||||||
|
model: TJSONObject;
|
||||||
|
i, j: Integer;
|
||||||
|
modelName: string;
|
||||||
|
canGen: Boolean;
|
||||||
|
list: TList<string>;
|
||||||
|
begin
|
||||||
|
SetLength(Result, 0);
|
||||||
|
jsonObj := TJSONObject.ParseJSONValue(JsonStr) as TJSONObject;
|
||||||
|
if not Assigned(jsonObj) then
|
||||||
|
exit;
|
||||||
|
|
||||||
|
list := TList<string>.Create;
|
||||||
|
try
|
||||||
|
models := jsonObj.GetValue('models') as TJSONArray;
|
||||||
|
if Assigned(models) then
|
||||||
|
begin
|
||||||
|
for i := 0 to models.Count - 1 do
|
||||||
|
begin
|
||||||
|
model := models.Items[i] as TJSONObject;
|
||||||
|
modelName := model.GetValue<string>('name');
|
||||||
|
|
||||||
|
if modelName.StartsWith('models/') then
|
||||||
|
modelName := modelName.Substring(7);
|
||||||
|
|
||||||
|
// Filter for Gemini models
|
||||||
|
if not modelName.ToLower.Contains('gemini') then
|
||||||
|
continue;
|
||||||
|
if modelName.ToLower.Contains('embedding') then
|
||||||
|
continue;
|
||||||
|
if modelName.ToLower.Contains('robotics') then
|
||||||
|
continue;
|
||||||
|
if modelName.ToLower.Contains('computer-use') then
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Filter for Generation capabilities
|
||||||
|
canGen := False;
|
||||||
|
methods := model.GetValue('supportedGenerationMethods') as TJSONArray;
|
||||||
|
if Assigned(methods) then
|
||||||
|
for j := 0 to methods.Count - 1 do
|
||||||
|
if (methods.Items[j].Value = 'generateContent') then
|
||||||
|
begin
|
||||||
|
canGen := True;
|
||||||
|
break;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if canGen then
|
||||||
|
list.Add(modelName);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
Result := list.ToArray;
|
||||||
|
finally
|
||||||
|
list.Free;
|
||||||
|
jsonObj.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// --- Context Caching ---
|
||||||
|
|
||||||
|
function TGeminiClient.CreateCache(const Content: string; const SysInstructions: string; const TTLSeconds: Integer): string;
|
||||||
|
var
|
||||||
|
httpClient: THTTPClient;
|
||||||
|
requestBody: TJSONObject;
|
||||||
|
contentsArray, partsArray: TJSONArray;
|
||||||
|
contentObj, partObj, sysObj: TJSONObject;
|
||||||
|
sysPartsArr: TJSONArray;
|
||||||
|
sysPart: TJSONObject;
|
||||||
|
response: IHTTPResponse;
|
||||||
|
stringStream: TStringStream;
|
||||||
|
cacheUrl: string;
|
||||||
|
respJson: TJSONObject;
|
||||||
|
begin
|
||||||
|
Result := '';
|
||||||
|
cacheUrl := Format('https://generativelanguage.googleapis.com/v1beta/cachedContents?key=%s', [TNetEncoding.URL.EncodeQuery(FApiKey)]);
|
||||||
|
|
||||||
|
httpClient := THTTPClient.Create;
|
||||||
|
requestBody := TJSONObject.Create;
|
||||||
|
try
|
||||||
|
requestBody.AddPair('model', FModelName);
|
||||||
|
|
||||||
|
partObj := TJSONObject.Create;
|
||||||
|
partObj.AddPair('text', Content);
|
||||||
|
|
||||||
|
partsArray := TJSONArray.Create;
|
||||||
|
partsArray.Add(partObj);
|
||||||
|
|
||||||
|
contentObj := TJSONObject.Create;
|
||||||
|
contentObj.AddPair('parts', partsArray);
|
||||||
|
contentObj.AddPair('role', 'user');
|
||||||
|
|
||||||
|
contentsArray := TJSONArray.Create;
|
||||||
|
contentsArray.Add(contentObj);
|
||||||
|
requestBody.AddPair('contents', contentsArray);
|
||||||
|
|
||||||
|
if not SysInstructions.IsEmpty then
|
||||||
|
begin
|
||||||
|
sysObj := TJSONObject.Create;
|
||||||
|
|
||||||
|
sysPartsArr := TJSONArray.Create;
|
||||||
|
sysPart := TJSONObject.Create;
|
||||||
|
sysPart.AddPair('text', SysInstructions);
|
||||||
|
sysPartsArr.Add(sysPart);
|
||||||
|
|
||||||
|
sysObj.AddPair('parts', sysPartsArr);
|
||||||
|
requestBody.AddPair('systemInstruction', sysObj);
|
||||||
|
end;
|
||||||
|
|
||||||
|
requestBody.AddPair('ttl', IntToStr(TTLSeconds) + 's');
|
||||||
|
|
||||||
|
stringStream := TStringStream.Create(requestBody.ToString, TEncoding.UTF8);
|
||||||
|
try
|
||||||
|
httpClient.CustomHeaders['Content-Type'] := 'application/json';
|
||||||
|
try
|
||||||
|
response := httpClient.Post(cacheUrl, stringStream);
|
||||||
|
except
|
||||||
|
on E: Exception do
|
||||||
|
raise Exception.Create('Cache Creation Failed: ' + E.Message);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Success is 200 OK or 201 Created
|
||||||
|
if (response.StatusCode = 200) or (response.StatusCode = 201) then
|
||||||
|
begin
|
||||||
|
respJson := TJSONObject.ParseJSONValue(response.ContentAsString(TEncoding.UTF8)) as TJSONObject;
|
||||||
|
try
|
||||||
|
if Assigned(respJson) then
|
||||||
|
Result := respJson.GetValue<string>('name');
|
||||||
|
finally
|
||||||
|
respJson.Free;
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
raise Exception.CreateFmt('Cache Error %d: %s', [response.StatusCode, response.ContentAsString(TEncoding.UTF8)]);
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
stringStream.Free;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
httpClient.Free;
|
||||||
|
requestBody.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TGeminiClient.StartChat: IGeminiChat;
|
||||||
|
begin
|
||||||
|
Result := TChatSession.Create(Self);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// --- TChatSession Implementation ---
|
||||||
|
|
||||||
|
constructor TChatSession.Create(const Client: IGeminiClient);
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FClient := Client;
|
||||||
|
FHistory := TList<TChatItem>.Create;
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TChatSession.Destroy;
|
||||||
|
begin
|
||||||
|
FHistory.Free;
|
||||||
|
inherited;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TChatSession.SendMessage(const Text: string): TGeminiResult;
|
||||||
|
var
|
||||||
|
item: TChatItem;
|
||||||
|
begin
|
||||||
|
item.Role := TChatRole.User;
|
||||||
|
item.Text := Text;
|
||||||
|
FHistory.Add(item);
|
||||||
|
|
||||||
|
Result := FClient.GenerateContent(FHistory.ToArray);
|
||||||
|
|
||||||
|
if not Result.Text.IsEmpty and not Result.Text.StartsWith('Error') then
|
||||||
|
begin
|
||||||
|
item.Role := TChatRole.Model;
|
||||||
|
item.Text := Result.Text;
|
||||||
|
FHistory.Add(item);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TChatSession.SendMessageStream(const Text: string; const OnDelta: TStreamCallback): TGeminiResult;
|
||||||
|
var
|
||||||
|
item: TChatItem;
|
||||||
|
begin
|
||||||
|
item.Role := TChatRole.User;
|
||||||
|
item.Text := Text;
|
||||||
|
FHistory.Add(item);
|
||||||
|
|
||||||
|
Result := FClient.GenerateContentStream(FHistory.ToArray, OnDelta);
|
||||||
|
|
||||||
|
if not Result.Text.IsEmpty and not Result.Text.StartsWith('Error') then
|
||||||
|
begin
|
||||||
|
item.Role := TChatRole.Model;
|
||||||
|
item.Text := Result.Text;
|
||||||
|
FHistory.Add(item);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TChatSession.GetHistory: TArray<TChatItem>;
|
||||||
|
begin
|
||||||
|
Result := FHistory.ToArray;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
unit Myc.System.Quota;
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
System.SysUtils,
|
||||||
|
System.Win.Registry,
|
||||||
|
Winapi.Windows,
|
||||||
|
System.Classes;
|
||||||
|
|
||||||
|
type
|
||||||
|
// Handles global quota tracking via Windows Registry
|
||||||
|
TGlobalQuota = class
|
||||||
|
private
|
||||||
|
const
|
||||||
|
REG_KEY = 'Software\Myc\GeminiTools\Quota';
|
||||||
|
|
||||||
|
// Value names
|
||||||
|
const
|
||||||
|
VAL_TOTAL_TOKENS = 'TotalTokens';
|
||||||
|
const
|
||||||
|
VAL_DAILY_TOKENS = 'DailyTokens';
|
||||||
|
const
|
||||||
|
VAL_LAST_RESET = 'LastReset'; // Manual reset timestamp
|
||||||
|
const
|
||||||
|
VAL_LAST_UPDATE = 'LastUpdate'; // Last modification timestamp
|
||||||
|
const
|
||||||
|
VAL_DAILY_DATE = 'DailyDate'; // Date for the daily counter reference
|
||||||
|
|
||||||
|
public
|
||||||
|
// Adds tokens to global and daily counters thread-safely
|
||||||
|
// Automatically resets the daily counter if the day has changed
|
||||||
|
class procedure AddTokens(Count: Integer);
|
||||||
|
|
||||||
|
// Reads the all-time total from registry
|
||||||
|
class function GetTotal: Int64;
|
||||||
|
|
||||||
|
// Reads the accumulated tokens for the current day
|
||||||
|
class function GetDailyTotal: Int64;
|
||||||
|
|
||||||
|
// Returns the date and time of the last modification (AddTokens)
|
||||||
|
class function GetLastUpdate: TDateTime;
|
||||||
|
|
||||||
|
// Returns the date and time of the last manual reset
|
||||||
|
class function GetLastReset: TDateTime;
|
||||||
|
|
||||||
|
// Resets all counters (Total and Daily) to zero
|
||||||
|
class procedure Reset;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
{ TGlobalQuota }
|
||||||
|
|
||||||
|
class procedure TGlobalQuota.AddTokens(Count: Integer);
|
||||||
|
var
|
||||||
|
reg: TRegistry;
|
||||||
|
mutex: THandle;
|
||||||
|
totalTokens, dailyTokens: Int64;
|
||||||
|
strValue: string;
|
||||||
|
lastDailyDate, today: TDateTime;
|
||||||
|
begin
|
||||||
|
if Count <= 0 then
|
||||||
|
exit;
|
||||||
|
|
||||||
|
// Use a named mutex to prevent race conditions between different processes
|
||||||
|
mutex := CreateMutex(nil, False, 'GlobalGeminiQuotaMutex');
|
||||||
|
if WaitForSingleObject(mutex, 2000) = WAIT_OBJECT_0 then
|
||||||
|
try
|
||||||
|
reg := TRegistry.Create(KEY_READ or KEY_WRITE);
|
||||||
|
try
|
||||||
|
reg.RootKey := HKEY_CURRENT_USER;
|
||||||
|
if reg.OpenKey(REG_KEY, True) then
|
||||||
|
begin
|
||||||
|
// 1. Handle Total Tokens
|
||||||
|
totalTokens := 0;
|
||||||
|
if reg.ValueExists(VAL_TOTAL_TOKENS) then
|
||||||
|
begin
|
||||||
|
strValue := reg.ReadString(VAL_TOTAL_TOKENS);
|
||||||
|
TryStrToInt64(strValue, totalTokens);
|
||||||
|
end;
|
||||||
|
totalTokens := totalTokens + Count;
|
||||||
|
|
||||||
|
// 2. Handle Daily Tokens
|
||||||
|
dailyTokens := 0;
|
||||||
|
lastDailyDate := 0;
|
||||||
|
today := Trunc(Now); // Date part only
|
||||||
|
|
||||||
|
if reg.ValueExists(VAL_DAILY_DATE) then
|
||||||
|
lastDailyDate := reg.ReadDate(VAL_DAILY_DATE);
|
||||||
|
|
||||||
|
// Check if we are on a new day
|
||||||
|
if lastDailyDate = today then
|
||||||
|
begin
|
||||||
|
// Same day, read existing daily count
|
||||||
|
if reg.ValueExists(VAL_DAILY_TOKENS) then
|
||||||
|
begin
|
||||||
|
strValue := reg.ReadString(VAL_DAILY_TOKENS);
|
||||||
|
TryStrToInt64(strValue, dailyTokens);
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
// New day (or first run), dailyTokens remains 0
|
||||||
|
// Update the reference date
|
||||||
|
reg.WriteDate(VAL_DAILY_DATE, today);
|
||||||
|
end;
|
||||||
|
|
||||||
|
dailyTokens := dailyTokens + Count;
|
||||||
|
|
||||||
|
// 3. Write updates
|
||||||
|
// Store numbers as String to support Int64 (Registry WriteInteger is 32-bit in older API wrappers)
|
||||||
|
reg.WriteString(VAL_TOTAL_TOKENS, totalTokens.ToString);
|
||||||
|
reg.WriteString(VAL_DAILY_TOKENS, dailyTokens.ToString);
|
||||||
|
reg.WriteDateTime(VAL_LAST_UPDATE, Now);
|
||||||
|
|
||||||
|
reg.CloseKey;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
reg.Free;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
ReleaseMutex(mutex);
|
||||||
|
CloseHandle(mutex);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
class function TGlobalQuota.GetTotal: Int64;
|
||||||
|
var
|
||||||
|
reg: TRegistry;
|
||||||
|
strValue: string;
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
reg := TRegistry.Create(KEY_READ);
|
||||||
|
try
|
||||||
|
reg.RootKey := HKEY_CURRENT_USER;
|
||||||
|
if reg.OpenKey(REG_KEY, False) then
|
||||||
|
begin
|
||||||
|
if reg.ValueExists(VAL_TOTAL_TOKENS) then
|
||||||
|
begin
|
||||||
|
strValue := reg.ReadString(VAL_TOTAL_TOKENS);
|
||||||
|
TryStrToInt64(strValue, Result);
|
||||||
|
end;
|
||||||
|
reg.CloseKey;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
reg.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
class function TGlobalQuota.GetDailyTotal: Int64;
|
||||||
|
var
|
||||||
|
reg: TRegistry;
|
||||||
|
strValue: string;
|
||||||
|
lastDailyDate: TDateTime;
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
reg := TRegistry.Create(KEY_READ);
|
||||||
|
try
|
||||||
|
reg.RootKey := HKEY_CURRENT_USER;
|
||||||
|
if reg.OpenKey(REG_KEY, False) then
|
||||||
|
begin
|
||||||
|
// Check if the stored daily count belongs to today
|
||||||
|
if reg.ValueExists(VAL_DAILY_DATE) then
|
||||||
|
begin
|
||||||
|
lastDailyDate := reg.ReadDate(VAL_DAILY_DATE);
|
||||||
|
if lastDailyDate = Trunc(Now) then
|
||||||
|
begin
|
||||||
|
if reg.ValueExists(VAL_DAILY_TOKENS) then
|
||||||
|
begin
|
||||||
|
strValue := reg.ReadString(VAL_DAILY_TOKENS);
|
||||||
|
TryStrToInt64(strValue, Result);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
// If date differs, Result remains 0 (logic effectively resets on read, though write happens in AddTokens)
|
||||||
|
end;
|
||||||
|
reg.CloseKey;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
reg.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
class function TGlobalQuota.GetLastUpdate: TDateTime;
|
||||||
|
var
|
||||||
|
reg: TRegistry;
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
reg := TRegistry.Create(KEY_READ);
|
||||||
|
try
|
||||||
|
reg.RootKey := HKEY_CURRENT_USER;
|
||||||
|
if reg.OpenKey(REG_KEY, False) then
|
||||||
|
begin
|
||||||
|
if reg.ValueExists(VAL_LAST_UPDATE) then
|
||||||
|
Result := reg.ReadDateTime(VAL_LAST_UPDATE);
|
||||||
|
reg.CloseKey;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
reg.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
class function TGlobalQuota.GetLastReset: TDateTime;
|
||||||
|
var
|
||||||
|
reg: TRegistry;
|
||||||
|
begin
|
||||||
|
Result := 0;
|
||||||
|
reg := TRegistry.Create(KEY_READ);
|
||||||
|
try
|
||||||
|
reg.RootKey := HKEY_CURRENT_USER;
|
||||||
|
if reg.OpenKey(REG_KEY, False) then
|
||||||
|
begin
|
||||||
|
if reg.ValueExists(VAL_LAST_RESET) then
|
||||||
|
Result := reg.ReadDateTime(VAL_LAST_RESET);
|
||||||
|
reg.CloseKey;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
reg.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
class procedure TGlobalQuota.Reset;
|
||||||
|
var
|
||||||
|
reg: TRegistry;
|
||||||
|
mutex: THandle;
|
||||||
|
begin
|
||||||
|
// Reset also needs Mutex protection to avoid conflicting writes with AddTokens
|
||||||
|
mutex := CreateMutex(nil, False, 'GlobalGeminiQuotaMutex');
|
||||||
|
if WaitForSingleObject(mutex, 2000) = WAIT_OBJECT_0 then
|
||||||
|
try
|
||||||
|
reg := TRegistry.Create(KEY_WRITE);
|
||||||
|
try
|
||||||
|
reg.RootKey := HKEY_CURRENT_USER;
|
||||||
|
if reg.OpenKey(REG_KEY, True) then
|
||||||
|
begin
|
||||||
|
reg.WriteString(VAL_TOTAL_TOKENS, '0');
|
||||||
|
reg.WriteString(VAL_DAILY_TOKENS, '0');
|
||||||
|
reg.WriteDate(VAL_DAILY_DATE, Trunc(Now));
|
||||||
|
reg.WriteDateTime(VAL_LAST_RESET, Now);
|
||||||
|
// We typically update LastUpdate on reset as well, or leave it as the last "Add" action.
|
||||||
|
// Here we update it to indicate change.
|
||||||
|
reg.WriteDateTime(VAL_LAST_UPDATE, Now);
|
||||||
|
reg.CloseKey;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
reg.Free;
|
||||||
|
end;
|
||||||
|
finally
|
||||||
|
ReleaseMutex(mutex);
|
||||||
|
CloseHandle(mutex);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
end.
|
||||||
@@ -47,20 +47,19 @@ class function TAstSchema.GetJsonType(Kind: TFieldKind): TJSONObject;
|
|||||||
Result.AddPair('type', ATypeName);
|
Result.AddPair('type', ATypeName);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function JConst(const AVal: string): TJSONObject;
|
// Use enum for const strings as it's widely supported
|
||||||
|
function JEnum(const AVal: string): TJSONObject;
|
||||||
|
var
|
||||||
|
arr: TJSONArray;
|
||||||
begin
|
begin
|
||||||
Result := TJSONObject.Create;
|
Result := JType('string');
|
||||||
Result.AddPair('const', AVal);
|
arr := TJSONArray.Create;
|
||||||
|
arr.Add(AVal);
|
||||||
|
Result.AddPair('enum', arr);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function JArray(AItems: TJSONValue): TJSONObject;
|
// Tuple Array (Fixed items)
|
||||||
begin
|
function JTupleArray(const AItems: array of TJSONValue): TJSONObject;
|
||||||
Result := JType('array');
|
|
||||||
Result.AddPair('items', AItems);
|
|
||||||
end;
|
|
||||||
|
|
||||||
// Combined function: Creates array from items and sets prefixItems + limits
|
|
||||||
function JFixedArray(const AItems: array of TJSONValue): TJSONObject;
|
|
||||||
var
|
var
|
||||||
arr: TJSONArray;
|
arr: TJSONArray;
|
||||||
item: TJSONValue;
|
item: TJSONValue;
|
||||||
@@ -68,19 +67,22 @@ class function TAstSchema.GetJsonType(Kind: TFieldKind): TJSONObject;
|
|||||||
begin
|
begin
|
||||||
Result := JType('array');
|
Result := JType('array');
|
||||||
|
|
||||||
|
// "items" as an array defines a Tuple schema in older drafts (supported by Gemini)
|
||||||
arr := TJSONArray.Create;
|
arr := TJSONArray.Create;
|
||||||
for item in AItems do
|
for item in AItems do
|
||||||
arr.AddElement(item);
|
arr.AddElement(item);
|
||||||
|
|
||||||
count := arr.Count;
|
count := arr.Count;
|
||||||
Result.AddPair('prefixItems', arr);
|
Result.AddPair('items', arr); // Items as Array = Tuple Definition
|
||||||
Result.AddPair('minItems', TJSONNumber.Create(count));
|
Result.AddPair('minItems', TJSONNumber.Create(count));
|
||||||
Result.AddPair('maxItems', TJSONNumber.Create(count));
|
Result.AddPair('maxItems', TJSONNumber.Create(count));
|
||||||
|
Result.AddPair('additionalItems', TJSONBool.Create(False)); // No extra items allowed
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function JTuple(AItems: TJSONValue): TJSONObject;
|
function JTuple(AItems: TJSONValue): TJSONObject;
|
||||||
begin
|
begin
|
||||||
Result := JFixedArray([JConst('Tuple'), AItems]);
|
// ["Tuple", [...]]
|
||||||
|
Result := JTupleArray([JEnum('Tuple'), AItems]);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function JAnyOf(AOptions: array of TJSONValue): TJSONObject;
|
function JAnyOf(AOptions: array of TJSONValue): TJSONObject;
|
||||||
@@ -97,7 +99,7 @@ class function TAstSchema.GetJsonType(Kind: TFieldKind): TJSONObject;
|
|||||||
{$endregion}
|
{$endregion}
|
||||||
|
|
||||||
var
|
var
|
||||||
pair, selItems, selTuple, entryContent, entryNode: TJSONObject;
|
pair, selTuple, entryNode: TJSONObject;
|
||||||
begin
|
begin
|
||||||
case Kind of
|
case Kind of
|
||||||
fkNode: Result := JDefRef('Node');
|
fkNode: Result := JDefRef('Node');
|
||||||
@@ -105,28 +107,54 @@ begin
|
|||||||
fkKeyword: Result := JDefRef('Key');
|
fkKeyword: Result := JDefRef('Key');
|
||||||
fkLambda: Result := JDefRef('Fn');
|
fkLambda: Result := JDefRef('Fn');
|
||||||
fkIdentifier: Result := JDefRef('Id');
|
fkIdentifier: Result := JDefRef('Id');
|
||||||
|
|
||||||
fkString: Result := JType('string');
|
fkString: Result := JType('string');
|
||||||
fkValue: Result := JAnyOf([JType('number'), JType('string'), JType('boolean')]);
|
|
||||||
fkNullableNode: Result := JAnyOf([JDefRef('Node'), JType('null')]);
|
fkValue:
|
||||||
fkArrayOfNodes: Result := JArray(JDefRef('Node'));
|
begin
|
||||||
|
// number | string | boolean
|
||||||
|
Result := JAnyOf([JType('number'), JType('string'), JType('boolean')]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
fkNullableNode:
|
||||||
|
begin
|
||||||
|
// Node | null
|
||||||
|
// Note: OpenAPI/Gemini often prefers 'nullable: true' over explicit null type,
|
||||||
|
// but anyOf [Node, {type: null}] works in full JSON schema.
|
||||||
|
Result := JAnyOf([JDefRef('Node'), JType('null')]);
|
||||||
|
end;
|
||||||
|
|
||||||
|
fkArrayOfNodes:
|
||||||
|
begin
|
||||||
|
// standard array of Nodes
|
||||||
|
Result := JType('array');
|
||||||
|
Result.AddPair('items', JDefRef('Node'));
|
||||||
|
end;
|
||||||
|
|
||||||
fkArrayOfPairs:
|
fkArrayOfPairs:
|
||||||
begin
|
begin
|
||||||
// [[Cond, Branch], ...]
|
// [[Cond, Branch], ...]
|
||||||
pair := JFixedArray([JDefRef('Node'), JDefRef('Node')]);
|
pair := JTupleArray([JDefRef('Node'), JDefRef('Node')]);
|
||||||
Result := JArray(pair);
|
Result := JType('array');
|
||||||
|
Result.AddPair('items', pair);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
fkPipeInputs:
|
fkPipeInputs:
|
||||||
begin
|
begin
|
||||||
// 1. Selector Tuple: ["Tuple", [Key, ...]]
|
// 1. Selector Tuple: ["Tuple", [Key, ...]]
|
||||||
selTuple := JTuple(JArray(JDefRef('Key')));
|
// Note: This is an array of Keys inside the Tuple structure
|
||||||
|
var keyArray := JType('array');
|
||||||
|
keyArray.AddPair('items', JDefRef('Key'));
|
||||||
|
selTuple := JTuple(keyArray);
|
||||||
|
|
||||||
// 2. Entry Node: ["Tuple", [Id, SelectorTuple]]
|
// 2. Entry Node: ["Tuple", [Id, SelectorTuple]]
|
||||||
entryNode := JTuple(JFixedArray([JDefRef('Id'), selTuple]));
|
entryNode := JTupleArray([JDefRef('Id'), selTuple]);
|
||||||
|
|
||||||
// 3. Outer Inputs List: ["Tuple", [EntryNode, ...]]
|
// 3. Wrap in ["Tuple", [Array of EntryNodes]]
|
||||||
Result := JTuple(JArray(entryNode));
|
var entriesArray := JType('array');
|
||||||
|
entriesArray.AddPair('items', entryNode);
|
||||||
|
|
||||||
|
Result := JTuple(entriesArray);
|
||||||
end;
|
end;
|
||||||
else
|
else
|
||||||
Result := TJSONObject.Create;
|
Result := TJSONObject.Create;
|
||||||
@@ -142,7 +170,11 @@ var
|
|||||||
anyOfNodes: TJSONArray;
|
anyOfNodes: TJSONArray;
|
||||||
tag: string;
|
tag: string;
|
||||||
fields: TList<AstFieldAttribute>;
|
fields: TList<AstFieldAttribute>;
|
||||||
minCount, maxCount: Integer;
|
itemsArr: TJSONArray;
|
||||||
|
|
||||||
|
// Properties Wrapper
|
||||||
|
props, programProp: TJSONObject;
|
||||||
|
reqArr: TJSONArray;
|
||||||
begin
|
begin
|
||||||
ctx := TRttiContext.Create;
|
ctx := TRttiContext.Create;
|
||||||
Result := TJSONObject.Create;
|
Result := TJSONObject.Create;
|
||||||
@@ -150,7 +182,7 @@ begin
|
|||||||
anyOfNodes := TJSONArray.Create;
|
anyOfNodes := TJSONArray.Create;
|
||||||
|
|
||||||
try
|
try
|
||||||
// 1. Scan all interfaces for AstTag metadata
|
// 1. Scan all interfaces
|
||||||
for typ in ctx.GetTypes do
|
for typ in ctx.GetTypes do
|
||||||
begin
|
begin
|
||||||
if typ.TypeKind <> tkInterface then
|
if typ.TypeKind <> tkInterface then
|
||||||
@@ -170,39 +202,41 @@ begin
|
|||||||
if tag = '' then
|
if tag = '' then
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Sort fields by index
|
// Sort fields
|
||||||
fields.Sort(
|
fields.Sort(
|
||||||
TComparer<AstFieldAttribute>
|
TComparer<AstFieldAttribute>
|
||||||
.Construct(function(const L, R: AstFieldAttribute): Integer begin Result := L.Index - R.Index; end)
|
.Construct(function(const L, R: AstFieldAttribute): Integer begin Result := L.Index - R.Index; end)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Construct Tuple Definition for this Tag: ["Tag", Arg1, Arg2]
|
||||||
nodeDef := TJSONObject.Create;
|
nodeDef := TJSONObject.Create;
|
||||||
nodeDef.AddPair('type', 'array');
|
nodeDef.AddPair('type', 'array');
|
||||||
|
|
||||||
var items := TJSONArray.Create;
|
itemsArr := TJSONArray.Create;
|
||||||
items.AddElement(TJSONObject.Create.AddPair('const', tag));
|
|
||||||
|
|
||||||
minCount := 1;
|
// Item 0: The Tag (as Const/Enum)
|
||||||
maxCount := 1;
|
var tagConst := TJSONObject.Create;
|
||||||
|
tagConst.AddPair('type', 'string');
|
||||||
|
var enumArr := TJSONArray.Create;
|
||||||
|
enumArr.Add(tag);
|
||||||
|
tagConst.AddPair('enum', enumArr);
|
||||||
|
itemsArr.AddElement(tagConst);
|
||||||
|
|
||||||
|
// Items 1..N: The Fields
|
||||||
for var f in fields do
|
for var f in fields do
|
||||||
begin
|
begin
|
||||||
inc(maxCount);
|
itemsArr.AddElement(GetJsonType(f.Kind));
|
||||||
// Only non-nullable fields increase minCount
|
|
||||||
if f.Kind <> fkNullableNode then
|
|
||||||
minCount := maxCount;
|
|
||||||
|
|
||||||
items.AddElement(GetJsonType(f.Kind));
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
nodeDef.AddPair('prefixItems', items);
|
nodeDef.AddPair('items', itemsArr);
|
||||||
nodeDef.AddPair('minItems', TJSONNumber.Create(minCount));
|
nodeDef.AddPair('minItems', TJSONNumber.Create(itemsArr.Count));
|
||||||
nodeDef.AddPair('maxItems', TJSONNumber.Create(maxCount));
|
nodeDef.AddPair('maxItems', TJSONNumber.Create(itemsArr.Count));
|
||||||
// additionalItems removed as maxItems guarantees strictness
|
nodeDef.AddPair('additionalItems', TJSONBool.Create(False));
|
||||||
|
|
||||||
|
// Add to defs
|
||||||
defs.AddPair(tag, nodeDef);
|
defs.AddPair(tag, nodeDef);
|
||||||
|
|
||||||
// Add reference to the global AstNode union
|
// Add to main Node Union
|
||||||
astNodeRef := TJSONObject.Create;
|
astNodeRef := TJSONObject.Create;
|
||||||
astNodeRef.AddPair('$ref', '#/$defs/' + tag);
|
astNodeRef.AddPair('$ref', '#/$defs/' + tag);
|
||||||
anyOfNodes.AddElement(astNodeRef);
|
anyOfNodes.AddElement(astNodeRef);
|
||||||
@@ -212,13 +246,29 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// 2. Define the base AstNode anyOf union
|
// 2. Define generic "Node" union
|
||||||
var astNodeBase := TJSONObject.Create;
|
var astNodeBase := TJSONObject.Create;
|
||||||
|
|
||||||
|
// WICHTIG: Das fehlende "type": "array" hat den Fehler verursacht.
|
||||||
|
astNodeBase.AddPair('type', 'array');
|
||||||
|
|
||||||
astNodeBase.AddPair('anyOf', anyOfNodes);
|
astNodeBase.AddPair('anyOf', anyOfNodes);
|
||||||
defs.AddPair('Node', astNodeBase);
|
defs.AddPair('Node', astNodeBase);
|
||||||
|
|
||||||
// 3. Assemble final Root Object
|
// 3. ROOT OBJECT WRAPPER
|
||||||
Result.AddPair('$ref', '#/$defs/Node');
|
Result.AddPair('type', 'object');
|
||||||
|
|
||||||
|
props := TJSONObject.Create;
|
||||||
|
programProp := TJSONObject.Create;
|
||||||
|
programProp.AddPair('$ref', '#/$defs/Node');
|
||||||
|
props.AddPair('program', programProp);
|
||||||
|
Result.AddPair('properties', props);
|
||||||
|
|
||||||
|
reqArr := TJSONArray.Create;
|
||||||
|
reqArr.Add('program');
|
||||||
|
Result.AddPair('required', reqArr);
|
||||||
|
|
||||||
|
Result.AddPair('additionalProperties', TJSONBool.Create(False));
|
||||||
Result.AddPair('$defs', defs);
|
Result.AddPair('$defs', defs);
|
||||||
|
|
||||||
finally
|
finally
|
||||||
|
|||||||
Reference in New Issue
Block a user