166 lines
4.6 KiB
C#
166 lines
4.6 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace ReverseLlama.Server;
|
|
|
|
internal sealed class ResponseTokenCounter
|
|
{
|
|
private const int MaxBufferedBytes = 4 * 1024 * 1024;
|
|
|
|
private readonly MemoryStream _buffer = new();
|
|
|
|
public void Add(ReadOnlySpan<byte> chunk)
|
|
{
|
|
if (chunk.Length == 0 || _buffer.Length >= MaxBufferedBytes)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var available = MaxBufferedBytes - (int)_buffer.Length;
|
|
var length = Math.Min(chunk.Length, available);
|
|
_buffer.Write(chunk[..length]);
|
|
}
|
|
|
|
public TokenCounts CountTokens()
|
|
{
|
|
if (_buffer.Length == 0)
|
|
{
|
|
return new TokenCounts(0, 0, 0);
|
|
}
|
|
|
|
var payload = Encoding.UTF8.GetString(_buffer.ToArray());
|
|
var totalPrompt = 0;
|
|
var totalCompletion = 0;
|
|
var parsedLines = false;
|
|
|
|
foreach (var rawLine in payload.Split('\n'))
|
|
{
|
|
var line = rawLine.Trim();
|
|
if (line.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (line.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
line = line["data:".Length..].Trim();
|
|
}
|
|
|
|
if (line.Equals("[DONE]", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (TryExtractTokenCountsFromJson(line, out var prompt, out var completion))
|
|
{
|
|
parsedLines = true;
|
|
totalPrompt += prompt;
|
|
totalCompletion += completion;
|
|
}
|
|
}
|
|
|
|
if (parsedLines)
|
|
{
|
|
return new TokenCounts(totalPrompt, totalCompletion, totalPrompt + totalCompletion);
|
|
}
|
|
|
|
if (TryExtractTokenCountsFromJson(payload, out var promptFallback, out var completionFallback))
|
|
{
|
|
return new TokenCounts(promptFallback, completionFallback, promptFallback + completionFallback);
|
|
}
|
|
|
|
return new TokenCounts(0, 0, 0);
|
|
}
|
|
|
|
private static bool TryExtractTokenCountsFromJson(string json, out int promptTokens, out int completionTokens)
|
|
{
|
|
promptTokens = 0;
|
|
completionTokens = 0;
|
|
|
|
try
|
|
{
|
|
using var document = JsonDocument.Parse(json);
|
|
ExtractTokenCounts(document.RootElement, out promptTokens, out completionTokens);
|
|
return promptTokens > 0 || completionTokens > 0;
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static void ExtractTokenCounts(JsonElement element, out int promptTokens, out int completionTokens)
|
|
{
|
|
promptTokens = 0;
|
|
completionTokens = 0;
|
|
|
|
if (element.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var item in element.EnumerateArray())
|
|
{
|
|
ExtractTokenCounts(item, out var itemPrompt, out var itemCompletion);
|
|
promptTokens += itemPrompt;
|
|
completionTokens += itemCompletion;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (element.ValueKind != JsonValueKind.Object)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (element.TryGetProperty("usage", out var usage) && usage.ValueKind == JsonValueKind.Object)
|
|
{
|
|
if (TryGetInt(usage, "prompt_tokens", out var pt))
|
|
{
|
|
promptTokens += pt;
|
|
}
|
|
|
|
if (TryGetInt(usage, "completion_tokens", out var ct))
|
|
{
|
|
completionTokens += ct;
|
|
}
|
|
|
|
if (promptTokens == 0 && completionTokens == 0)
|
|
{
|
|
if (TryGetInt(usage, "input_tokens", out var it))
|
|
{
|
|
promptTokens += it;
|
|
}
|
|
|
|
if (TryGetInt(usage, "output_tokens", out var ot))
|
|
{
|
|
completionTokens += ot;
|
|
}
|
|
}
|
|
|
|
if (promptTokens > 0 || completionTokens > 0)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (TryGetInt(element, "prompt_eval_count", out var promptEval))
|
|
{
|
|
promptTokens += promptEval;
|
|
}
|
|
|
|
if (TryGetInt(element, "eval_count", out var eval))
|
|
{
|
|
completionTokens += eval;
|
|
}
|
|
}
|
|
|
|
private static bool TryGetInt(JsonElement element, string propertyName, out int value)
|
|
{
|
|
value = 0;
|
|
return element.TryGetProperty(propertyName, out var property)
|
|
&& property.ValueKind == JsonValueKind.Number
|
|
&& property.TryGetInt32(out value);
|
|
}
|
|
}
|
|
|
|
internal sealed record TokenCounts(int PromptTokens, int CompletionTokens, int TotalTokens);
|