This commit is contained in:
2026-07-20 10:44:56 +02:00
parent 7574dbfd08
commit b14d853da9
7 changed files with 275 additions and 77 deletions

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Root Type="DevExpress.CodeRush.Foundation.CodePlaces.Options.FavoritesListContainer">
<Options Language="Neutral">
<Groups />
</Options>
</Root>

View File

@@ -37,10 +37,18 @@
<Label Grid.Row="0" Grid.Column="2" Content="API-Key:" VerticalAlignment="Center" Margin="10,0,0,0"/>
<TextBox Grid.Row="0" Grid.Column="3" Grid.ColumnSpan="2" x:Name="TxtApiKey" Margin="0,2" VerticalContentAlignment="Center" Text="mu9ZPuUXJ4aVGLU1WL2RfPCeSMRmJREwyjVJBP7bHfSz8quZRqN7e7UwnwCDi8GZ"/>
<Label Grid.Row="1" Grid.Column="0" Content="Modell:" VerticalAlignment="Center"/>
<ComboBox Grid.Row="1" Grid.Column="1" x:Name="CmbModel" Margin="0,2" IsEditable="True" VerticalContentAlignment="Center" Text="gemma3_12b_max_context:latest"/>
<Label Grid.Row="1" Grid.Column="0" Content="Modelle:" VerticalAlignment="Top"/>
<ListBox Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="3" x:Name="LstModels" Margin="0,2" MaxHeight="100"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ToolTip="Für jedes angehakte Modell wird eine eigene Analyse durchgeführt (eigener Ergebnis-Tab).">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Name}" IsChecked="{Binding IsSelected, Mode=TwoWay}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Grid.Row="1" Grid.Column="4" x:Name="BtnLoadModels" Content="Modelle laden" Width="120" Margin="10,2,0,2" Click="BtnLoadModels_Click"/>
<Button Grid.Row="1" Grid.Column="4" x:Name="BtnLoadModels" Content="Modelle laden" Width="120" Margin="10,2,0,2" VerticalAlignment="Top" Click="BtnLoadModels_Click"/>
</Grid>
</GroupBox>
@@ -74,7 +82,7 @@
ToolTip="Optional: hält die Antwort kurz. Zählt mit wenigen Tokens in prompt_eval_count mit."/>
<Label Grid.Row="1" Grid.Column="2" Content="CustomerId:" VerticalAlignment="Center" Margin="10,0,0,0"/>
<TextBox Grid.Row="1" Grid.Column="3" x:Name="TxtCustomerId" Margin="0,2" VerticalContentAlignment="Center"
<TextBox Grid.Row="1" Grid.Column="3" x:Name="TxtCustomerId" Margin="0,2" VerticalContentAlignment="Center" Text="token-analyzer"
ToolTip="Optional: wird als 'customerid' mitgesendet."/>
</Grid>
</GroupBox>
@@ -94,7 +102,15 @@
</StackPanel>
<GroupBox Grid.Row="4" Header="Ergebnis" Padding="5">
<DataGrid x:Name="GridResults" AutoGenerateColumns="False" IsReadOnly="True" CanUserAddRows="False"
<TabControl x:Name="TabResults">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ModelName}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<DataGrid ItemsSource="{Binding Rows}" AutoGenerateColumns="False" IsReadOnly="True" CanUserAddRows="False"
HeadersVisibility="Column" GridLinesVisibility="All">
<DataGrid.Columns>
<DataGridTextColumn Header="Zielgröße" Binding="{Binding TargetChars, StringFormat=N0}" Width="Auto"/>
@@ -102,11 +118,15 @@
<DataGridTextColumn Header="Zeichen / 3" Binding="{Binding EstDiv3, StringFormat=N0}" Width="Auto"/>
<DataGridTextColumn Header="Zeichen / 3,5" Binding="{Binding EstDiv3_5, StringFormat=N0}" Width="Auto"/>
<DataGridTextColumn Header="cl100k" Binding="{Binding Cl100k, StringFormat=N0}" Width="Auto"/>
<DataGridTextColumn Header="cl100k - 10%" Binding="{Binding Cl100kMinus10, StringFormat=N0}" Width="Auto"/>
<DataGridTextColumn Header="prompt_eval_count" Binding="{Binding PromptEvalCount, StringFormat=N0}" Width="Auto"/>
<DataGridTextColumn Header="Dauer (ms)" Binding="{Binding Duration, StringFormat=N0}" Width="Auto"/>
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</GroupBox>
<StatusBar Grid.Row="5" Margin="0,5,0,0">

View File

@@ -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
/// </summary>
public partial class MainWindow : Window
{
public ObservableCollection<TokenAnalyzeRow> Rows { get; } = new ObservableCollection<TokenAnalyzeRow>();
public ObservableCollection<ModelSelectionItem> Models { get; } = new ObservableCollection<ModelSelectionItem>();
public ObservableCollection<ModelAnalyzeResult> ModelResults { get; } = new ObservableCollection<ModelAnalyzeResult>();
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<string>(settings.SelectedModels ?? new List<string>());
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<string>(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,43 +245,57 @@ namespace AiTokenAnalyzer
TxtStatus.Text = "Abbruch angefordert (laufender Request wird noch abgewartet)...";
}
private async Task runAnalysisAsync(string text, List<int> sizes, string model, bool repeat, string system_prompt, string customer_id, int? num_ctx, CancellationToken token)
private async Task runAnalysisAsync(string text, List<int> sizes, List<string> 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<KeyValuePair<TokenAnalyzeRow, string>>();
// Text-Chunks vorbereiten (für alle Modelle identisch)
var chunks = new List<KeyValuePair<int, string>>();
foreach (var size in sizes.Distinct().OrderBy(x => x))
{
var chunk = buildChunk(text, size, repeat);
chunks.Add(new KeyValuePair<int, string>(size, buildChunk(text, size, repeat)));
var row = new TokenAnalyzeRow
{
TargetChars = size,
ActualChars = chunk.Length,
Status = "Wartet"
};
Rows.Add(row);
chunks.Add(new KeyValuePair<TokenAnalyzeRow, string>(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<int>();
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;
for (var i = 0; i < chunks.Count; i++)
{
var row = result.Rows[i];
if (token.IsCancellationRequested)
{
@@ -194,27 +304,27 @@ namespace AiTokenAnalyzer
}
index++;
TxtStatus.Text = $"Sende Request {index}/{chunks.Count} ({row.ActualChars:N0} Zeichen) an '{model}'...";
TxtStatus.Text = $"Request {index}/{total}: '{result.ModelName}', {row.ActualChars:N0} Zeichen...";
row.Status = "Läuft...";
var payload = buildPayload(model, system_prompt, pair.Value, customer_id, num_ctx);
var payload = buildPayload(result.ModelName, system_prompt, chunks[i].Value, customer_id, num_ctx);
try
{
var result = await Task.Run(() =>
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 = result.prompt_eval_count;
row.Duration = result.duration;
row.PromptEvalCount = request_result.prompt_eval_count;
row.Duration = request_result.duration;
if (result.prompt_eval_count is long)
if (request_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 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";
}
@@ -223,6 +333,7 @@ namespace AiTokenAnalyzer
row.Status = "Fehler: " + ex.Message;
}
}
}
TxtStatus.Text = token.IsCancellationRequested ? "Analyse abgebrochen." : "Analyse abgeschlossen.";
}

View File

@@ -0,0 +1,14 @@
using System.Collections.ObjectModel;
namespace AiTokenAnalyzer
{
/// <summary>
/// Analyse-Ergebnis eines Modells (ein Tab in der Ergebnisansicht).
/// </summary>
public class ModelAnalyzeResult
{
public string ModelName { get; set; }
public ObservableCollection<TokenAnalyzeRow> Rows { get; } = new ObservableCollection<TokenAnalyzeRow>();
}
}

View File

@@ -0,0 +1,22 @@
using System.ComponentModel;
namespace AiTokenAnalyzer
{
/// <summary>
/// Ein auswählbares Modell in der Modell-Liste.
/// </summary>
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;
}
}

View File

@@ -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;

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace AiTokenAnalyzer
{
/// <summary>
/// Beim Schließen gespeicherte / beim Öffnen geladene Eingabewerte.
/// </summary>
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<string> Models { get; set; } = new List<string>();
public List<string> SelectedModels { get; set; } = new List<string>();
}
}