mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-17 09:18:00 +00:00
Added the exact token count where the provider reports it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c95cc5bacc
commit
0aa3b23b81
21 files changed
+598
-7
No files matched your search
@@ -52,6 +52,44 @@ public sealed class ContentText : IContent
|
||||
|
||||
public List<ToolInvocationTrace> ToolInvocations { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// What the provider said everything sent along with this answer cost, where it said anything.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Kept on the answer rather than beside the chat, so that it is stored, loaded, and exported
|
||||
/// with the message it belongs to -- and so that it goes away when the message does. An edited
|
||||
/// or regenerated answer removes its block, which removes these numbers with it, and the chat
|
||||
/// falls back to the estimate instead of carrying a figure for a conversation which no longer
|
||||
/// exists. Null for every answer written before this was recorded, and at every provider which
|
||||
/// reports nothing.
|
||||
///
|
||||
/// Two plain numbers rather than a <see cref="TokenUsage"/>: that type only ever comes out of
|
||||
/// its own factory, which is what keeps an impossible usage from existing, and a stored field
|
||||
/// has to be readable back by the serializer.
|
||||
/// </remarks>
|
||||
public int? ReportedPromptTokens { get; set; }
|
||||
|
||||
/// <inheritdoc cref="ReportedPromptTokens"/>
|
||||
public int? ReportedCompletionTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Which model the numbers above were charged for.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A token count belongs to the tokenizer which produced it. Switch the model of a chat, and
|
||||
/// the same conversation is worth a different number of tokens -- so the reported one stops
|
||||
/// being an answer about the request which is about to be sent, and the estimate, wrong as it
|
||||
/// is, is at least wrong about the right model.
|
||||
/// </remarks>
|
||||
public string? ReportedForModel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// What the provider said this exchange cost, which is what the next request carries as its
|
||||
/// history.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public TokenUsage ReportedTokens => TokenUsage.OfReported(this.ReportedPromptTokens, this.ReportedCompletionTokens);
|
||||
|
||||
[JsonIgnore]
|
||||
public ToolRuntimeStatus ToolRuntimeStatus { get; set; } = new();
|
||||
|
||||
@@ -187,6 +225,18 @@ public sealed class ContentText : IContent
|
||||
// Merge the sources:
|
||||
this.Sources.MergeSources(contentStreamChunk.Sources);
|
||||
|
||||
//
|
||||
// Keep what the provider says the request cost. It arrives on one line of
|
||||
// the stream, usually the last one, and only where the provider reports it
|
||||
// at all -- so the previous value is kept rather than cleared:
|
||||
//
|
||||
if (contentStreamChunk.Usage.IsKnown)
|
||||
{
|
||||
this.ReportedPromptTokens = contentStreamChunk.Usage.PromptTokens;
|
||||
this.ReportedCompletionTokens = contentStreamChunk.Usage.CompletionTokens;
|
||||
this.ReportedForModel = chatModel.Id;
|
||||
}
|
||||
|
||||
// Notify the UI that the content has changed,
|
||||
// depending on the energy saving mode:
|
||||
var now = DateTimeOffset.Now;
|
||||
|
||||
@@ -57,6 +57,21 @@ public sealed record ConversationParts
|
||||
/// </summary>
|
||||
public int Images { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Which of the texts above are the message being written right now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A marker, not a further part: everything named here also stands in <see cref="GrowingTexts"/>,
|
||||
/// and counting the parts counts each of them exactly once. It exists because the two halves of
|
||||
/// the number answer different questions. What the conversation has cost so far can be had
|
||||
/// exactly, from the provider which charged for it; what is about to be added to it can only be
|
||||
/// estimated. Told as one number, nobody can see which half they are looking at.
|
||||
/// </remarks>
|
||||
public IReadOnlyList<string> DraftTexts { get; init; } = [];
|
||||
|
||||
/// <inheritdoc cref="DraftTexts"/>
|
||||
public IReadOnlyList<FileAttachment> DraftDocuments { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Collects what a conversation would send.
|
||||
/// </summary>
|
||||
@@ -131,18 +146,36 @@ public sealed record ConversationParts
|
||||
}
|
||||
}
|
||||
|
||||
var draftTexts = new List<string>();
|
||||
var draftDocuments = new List<FileAttachment>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(draft))
|
||||
{
|
||||
growing.Add(draft);
|
||||
draftTexts.Add(draft);
|
||||
}
|
||||
|
||||
if (draftAttachments is not null)
|
||||
{
|
||||
var documentsBefore = documents.Count;
|
||||
Sort(draftAttachments, documents, ref images);
|
||||
|
||||
//
|
||||
// Whatever sorting just appended is what the composer carries. Read off the list
|
||||
// rather than sorted a second time, so that a change to what counts as a document
|
||||
// cannot start meaning two different things in one method.
|
||||
//
|
||||
draftDocuments.AddRange(documents.Skip(documentsBefore));
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
Texts = texts,
|
||||
GrowingTexts = growing,
|
||||
Documents = documents,
|
||||
Images = imagesAreSent ? images : 0,
|
||||
DraftTexts = draftTexts,
|
||||
DraftDocuments = draftDocuments,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,27 @@ public readonly record struct ConversationTokens
|
||||
/// </remarks>
|
||||
public bool IsEstimate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// How much of <see cref="Tokens"/> is the message being written right now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Kept apart from the rest for the same reason the statements above are kept apart: what the
|
||||
/// conversation has already cost is something a provider can be asked about, while a sentence
|
||||
/// nobody has sent yet can only be estimated. What the conversation costs without it is this
|
||||
/// subtracted from <see cref="Tokens"/>.
|
||||
/// </remarks>
|
||||
public int DraftTokens { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the conversation so far was counted by the provider rather than by this app.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// True once a provider has stated what a request of this conversation cost, which makes
|
||||
/// everything but the draft an exact number. It says nothing about the draft, which stays an
|
||||
/// estimate either way -- nobody has charged for that one yet.
|
||||
/// </remarks>
|
||||
public bool HistoryIsReported { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// How much the model reads, where anybody has stated it.
|
||||
/// </summary>
|
||||
|
||||
Reference in new issue
Block a user