diff --git a/Tools/AiTokenAnalyzer/.cr/personal/FavoritesList/List.xml b/Tools/AiTokenAnalyzer/.cr/personal/FavoritesList/List.xml
new file mode 100644
index 000000000..a60e5ed6c
--- /dev/null
+++ b/Tools/AiTokenAnalyzer/.cr/personal/FavoritesList/List.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Tools/AiTokenAnalyzer/AiTokenAnalyzer/MainWindow.xaml b/Tools/AiTokenAnalyzer/AiTokenAnalyzer/MainWindow.xaml
index 8e04c91f8..00b89f353 100644
--- a/Tools/AiTokenAnalyzer/AiTokenAnalyzer/MainWindow.xaml
+++ b/Tools/AiTokenAnalyzer/AiTokenAnalyzer/MainWindow.xaml
@@ -37,10 +37,18 @@
-
-
+
+
+
+
+
+
+
+
-
+
@@ -74,7 +82,7 @@
ToolTip="Optional: hält die Antwort kurz. Zählt mit wenigen Tokens in prompt_eval_count mit."/>
-
@@ -94,19 +102,31 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Tools/AiTokenAnalyzer/AiTokenAnalyzer/MainWindow.xaml.cs b/Tools/AiTokenAnalyzer/AiTokenAnalyzer/MainWindow.xaml.cs
index eab59a567..3c53dd23c 100644
--- a/Tools/AiTokenAnalyzer/AiTokenAnalyzer/MainWindow.xaml.cs
+++ b/Tools/AiTokenAnalyzer/AiTokenAnalyzer/MainWindow.xaml.cs
@@ -3,12 +3,15 @@ using SharpToken;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
+using System.Xml.Serialization;
namespace AiTokenAnalyzer
{
@@ -17,7 +20,8 @@ namespace AiTokenAnalyzer
///
public partial class MainWindow : Window
{
- public ObservableCollection Rows { get; } = new ObservableCollection();
+ public ObservableCollection Models { get; } = new ObservableCollection();
+ public ObservableCollection ModelResults { get; } = new ObservableCollection();
private CancellationTokenSource _Cts;
@@ -25,7 +29,95 @@ namespace AiTokenAnalyzer
{
InitializeComponent();
- GridResults.ItemsSource = Rows;
+ LstModels.ItemsSource = Models;
+ TabResults.ItemsSource = ModelResults;
+
+ loadSettings();
+
+ if (Models.Count == 0)
+ Models.Add(new ModelSelectionItem { Name = "gemma3_12b_max_context:latest", IsSelected = true });
+ }
+
+ protected override void OnClosing(CancelEventArgs e)
+ {
+ saveSettings();
+ base.OnClosing(e);
+ }
+
+ private static string getSettingsPath()
+ => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AiTokenAnalyzer", "settings.xml");
+
+ private void loadSettings()
+ {
+ try
+ {
+ var path = getSettingsPath();
+
+ if (!File.Exists(path))
+ return;
+
+ UserSettings settings;
+
+ using (var stream = File.OpenRead(path))
+ settings = (UserSettings)new XmlSerializer(typeof(UserSettings)).Deserialize(stream);
+
+ if (settings == null)
+ return;
+
+ TxtUrl.Text = settings.Url ?? TxtUrl.Text;
+ TxtApiKey.Text = settings.ApiKey ?? TxtApiKey.Text;
+ TxtSizes.Text = settings.Sizes ?? TxtSizes.Text;
+ TxtNumCtx.Text = settings.NumCtx ?? TxtNumCtx.Text;
+ ChkRepeat.IsChecked = settings.Repeat;
+ TxtSystemPrompt.Text = settings.SystemPrompt ?? TxtSystemPrompt.Text;
+ TxtCustomerId.Text = settings.CustomerId ?? TxtCustomerId.Text;
+ TxtInput.Text = settings.InputText ?? TxtInput.Text;
+
+ if (settings.Models is object && settings.Models.Count > 0)
+ {
+ var selected = new HashSet(settings.SelectedModels ?? new List());
+
+ Models.Clear();
+
+ foreach (var name in settings.Models)
+ Models.Add(new ModelSelectionItem { Name = name, IsSelected = selected.Contains(name) });
+ }
+ }
+ catch
+ {
+ // Defekte Settings-Datei ignorieren, Defaults verwenden
+ }
+ }
+
+ private void saveSettings()
+ {
+ try
+ {
+ var settings = new UserSettings
+ {
+ Url = TxtUrl.Text,
+ ApiKey = TxtApiKey.Text,
+ Sizes = TxtSizes.Text,
+ NumCtx = TxtNumCtx.Text,
+ Repeat = ChkRepeat.IsChecked == true,
+ SystemPrompt = TxtSystemPrompt.Text,
+ CustomerId = TxtCustomerId.Text,
+ InputText = TxtInput.Text,
+ Models = Models.Select(m => m.Name).ToList(),
+ SelectedModels = Models.Where(m => m.IsSelected).Select(m => m.Name).ToList()
+ };
+
+ var path = getSettingsPath();
+
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
+
+ using (var stream = File.Create(path))
+ new XmlSerializer(typeof(UserSettings)).Serialize(stream, settings);
+ }
+ catch
+ {
+ // Speichern der Settings darf das Schließen nicht verhindern
+ }
}
private OllamaApiClient createClient()
@@ -55,10 +147,14 @@ namespace AiTokenAnalyzer
}
var model_names = response.Data.Select(m => m.ModelName).OrderBy(x => x).ToList();
- var current = CmbModel.Text;
- CmbModel.ItemsSource = model_names;
- CmbModel.Text = current;
+ // Bisherige Auswahl beibehalten
+ var selected = new HashSet(Models.Where(m => m.IsSelected).Select(m => m.Name));
+
+ Models.Clear();
+
+ foreach (var name in model_names)
+ Models.Add(new ModelSelectionItem { Name = name, IsSelected = selected.Contains(name) });
TxtStatus.Text = $"{model_names.Count} Modelle geladen.";
}
@@ -82,11 +178,11 @@ namespace AiTokenAnalyzer
return;
}
- var model = CmbModel.Text?.Trim();
+ var models = Models.Where(m => m.IsSelected).Select(m => m.Name).ToList();
- if (string.IsNullOrEmpty(model))
+ if (models.Count == 0)
{
- MessageBox.Show("Bitte ein Modell angeben.", Title, MessageBoxButton.OK, MessageBoxImage.Warning);
+ MessageBox.Show("Bitte mindestens ein Modell auswählen.", Title, MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
@@ -132,7 +228,7 @@ namespace AiTokenAnalyzer
try
{
- await runAnalysisAsync(text, sizes, model, repeat, system_prompt, customer_id, num_ctx, _Cts.Token);
+ await runAnalysisAsync(text, sizes, models, repeat, system_prompt, customer_id, num_ctx, _Cts.Token);
}
finally
{
@@ -149,78 +245,93 @@ namespace AiTokenAnalyzer
TxtStatus.Text = "Abbruch angefordert (laufender Request wird noch abgewartet)...";
}
- private async Task runAnalysisAsync(string text, List sizes, string model, bool repeat, string system_prompt, string customer_id, int? num_ctx, CancellationToken token)
+ private async Task runAnalysisAsync(string text, List sizes, List models, bool repeat, string system_prompt, string customer_id, int? num_ctx, CancellationToken token)
{
- Rows.Clear();
+ ModelResults.Clear();
- // Zeilen + Text-Chunks vorbereiten
- var chunks = new List>();
+ // Text-Chunks vorbereiten (für alle Modelle identisch)
+ var chunks = new List>();
foreach (var size in sizes.Distinct().OrderBy(x => x))
- {
- var chunk = buildChunk(text, size, repeat);
+ chunks.Add(new KeyValuePair(size, buildChunk(text, size, repeat)));
- var row = new TokenAnalyzeRow
- {
- TargetChars = size,
- ActualChars = chunk.Length,
- Status = "Wartet"
- };
-
- Rows.Add(row);
- chunks.Add(new KeyValuePair(row, chunk));
- }
-
- // Lokale cl100k-Abschätzung
+ // Lokale cl100k-Abschätzung (einmalig, modellunabhängig)
TxtStatus.Text = "Berechne cl100k-Abschätzung...";
var encoding = await Task.Run(() => GptEncoding.GetEncoding("cl100k_base"));
+ var cl100k_counts = new List();
foreach (var pair in chunks)
- pair.Key.Cl100k = await Task.Run(() => encoding.Encode(pair.Value).Count);
+ cl100k_counts.Add(await Task.Run(() => encoding.Encode(pair.Value).Count));
- // LLM-Requests (sequenziell)
+ // Pro Modell einen Tab mit allen Größen anlegen
+ foreach (var model in models)
+ {
+ var result = new ModelAnalyzeResult { ModelName = model };
+
+ for (var i = 0; i < chunks.Count; i++)
+ {
+ result.Rows.Add(new TokenAnalyzeRow
+ {
+ TargetChars = chunks[i].Key,
+ ActualChars = chunks[i].Value.Length,
+ Cl100k = cl100k_counts[i],
+ Status = "Wartet"
+ });
+ }
+
+ ModelResults.Add(result);
+ }
+
+ // LLM-Requests (sequenziell, Modell für Modell)
var client = createClient();
+ var total = models.Count * chunks.Count;
var index = 0;
- foreach (var pair in chunks)
+ foreach (var result in ModelResults)
{
- var row = pair.Key;
+ if (!token.IsCancellationRequested)
+ TabResults.SelectedItem = result;
- if (token.IsCancellationRequested)
+ for (var i = 0; i < chunks.Count; i++)
{
- row.Status = "Abgebrochen";
- continue;
- }
+ var row = result.Rows[i];
- index++;
- TxtStatus.Text = $"Sende Request {index}/{chunks.Count} ({row.ActualChars:N0} Zeichen) an '{model}'...";
- row.Status = "Läuft...";
-
- var payload = buildPayload(model, system_prompt, pair.Value, customer_id, num_ctx);
-
- try
- {
- var result = await Task.Run(() =>
+ if (token.IsCancellationRequested)
{
- var response = client.SendAiMessageRequest(payload, out string message, out string response_model, out int? duration, out long? prompt_eval_count, out long? eval_count);
+ row.Status = "Abgebrochen";
+ continue;
+ }
- return new { response, duration, prompt_eval_count };
- });
+ index++;
+ TxtStatus.Text = $"Request {index}/{total}: '{result.ModelName}', {row.ActualChars:N0} Zeichen...";
+ row.Status = "Läuft...";
- row.PromptEvalCount = result.prompt_eval_count;
- row.Duration = result.duration;
+ var payload = buildPayload(result.ModelName, system_prompt, chunks[i].Value, customer_id, num_ctx);
- if (result.prompt_eval_count is long)
- row.Status = "OK";
- else if (result.response is object && !result.response.Success)
- row.Status = "Fehler: " + result.response.ToErrorString();
- else
- row.Status = "Fehler: keine prompt_eval_count in der Antwort";
- }
- catch (Exception ex)
- {
- row.Status = "Fehler: " + ex.Message;
+ try
+ {
+ var request_result = await Task.Run(() =>
+ {
+ var response = client.SendAiMessageRequest(payload, out string message, out string response_model, out int? duration, out long? prompt_eval_count, out long? eval_count);
+
+ return new { response, duration, prompt_eval_count };
+ });
+
+ row.PromptEvalCount = request_result.prompt_eval_count;
+ row.Duration = request_result.duration;
+
+ if (request_result.prompt_eval_count is long)
+ row.Status = "OK";
+ else if (request_result.response is object && !request_result.response.Success)
+ row.Status = "Fehler: " + request_result.response.ToErrorString();
+ else
+ row.Status = "Fehler: keine prompt_eval_count in der Antwort";
+ }
+ catch (Exception ex)
+ {
+ row.Status = "Fehler: " + ex.Message;
+ }
}
}
diff --git a/Tools/AiTokenAnalyzer/AiTokenAnalyzer/ModelAnalyzeResult.cs b/Tools/AiTokenAnalyzer/AiTokenAnalyzer/ModelAnalyzeResult.cs
new file mode 100644
index 000000000..991993100
--- /dev/null
+++ b/Tools/AiTokenAnalyzer/AiTokenAnalyzer/ModelAnalyzeResult.cs
@@ -0,0 +1,14 @@
+using System.Collections.ObjectModel;
+
+namespace AiTokenAnalyzer
+{
+ ///
+ /// Analyse-Ergebnis eines Modells (ein Tab in der Ergebnisansicht).
+ ///
+ public class ModelAnalyzeResult
+ {
+ public string ModelName { get; set; }
+
+ public ObservableCollection Rows { get; } = new ObservableCollection();
+ }
+}
diff --git a/Tools/AiTokenAnalyzer/AiTokenAnalyzer/ModelSelectionItem.cs b/Tools/AiTokenAnalyzer/AiTokenAnalyzer/ModelSelectionItem.cs
new file mode 100644
index 000000000..e1f6bd496
--- /dev/null
+++ b/Tools/AiTokenAnalyzer/AiTokenAnalyzer/ModelSelectionItem.cs
@@ -0,0 +1,22 @@
+using System.ComponentModel;
+
+namespace AiTokenAnalyzer
+{
+ ///
+ /// Ein auswählbares Modell in der Modell-Liste.
+ ///
+ public class ModelSelectionItem : INotifyPropertyChanged
+ {
+ private bool _IsSelected;
+
+ public string Name { get; set; }
+
+ public bool IsSelected
+ {
+ get => _IsSelected;
+ set { _IsSelected = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSelected))); }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+ }
+}
diff --git a/Tools/AiTokenAnalyzer/AiTokenAnalyzer/TokenAnalyzeRow.cs b/Tools/AiTokenAnalyzer/AiTokenAnalyzer/TokenAnalyzeRow.cs
index 52b2bedb6..b6654f19c 100644
--- a/Tools/AiTokenAnalyzer/AiTokenAnalyzer/TokenAnalyzeRow.cs
+++ b/Tools/AiTokenAnalyzer/AiTokenAnalyzer/TokenAnalyzeRow.cs
@@ -22,9 +22,12 @@ namespace AiTokenAnalyzer
public int? Cl100k
{
get => _Cl100k;
- set { _Cl100k = value; onPropertyChanged(nameof(Cl100k)); }
+ set { _Cl100k = value; onPropertyChanged(nameof(Cl100k)); onPropertyChanged(nameof(Cl100kMinus10)); }
}
+ public int? Cl100kMinus10
+ => _Cl100k is int c ? (int?)(int)Math.Round(c * 0.9) : null;
+
public long? PromptEvalCount
{
get => _PromptEvalCount;
diff --git a/Tools/AiTokenAnalyzer/AiTokenAnalyzer/UserSettings.cs b/Tools/AiTokenAnalyzer/AiTokenAnalyzer/UserSettings.cs
new file mode 100644
index 000000000..64ed6271d
--- /dev/null
+++ b/Tools/AiTokenAnalyzer/AiTokenAnalyzer/UserSettings.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+
+namespace AiTokenAnalyzer
+{
+ ///
+ /// Beim Schließen gespeicherte / beim Öffnen geladene Eingabewerte.
+ ///
+ public class UserSettings
+ {
+ public string Url { get; set; }
+ public string ApiKey { get; set; }
+ public string Sizes { get; set; }
+ public string NumCtx { get; set; }
+ public bool Repeat { get; set; } = true;
+ public string SystemPrompt { get; set; }
+ public string CustomerId { get; set; }
+ public string InputText { get; set; }
+
+ public List Models { get; set; } = new List();
+ public List SelectedModels { get; set; } = new List();
+ }
+}