mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 00:13:38 +00:00
Kept the reported token usage as one record on the answer
This commit is contained in:
parent
ceb78ff9b7
commit
4918858650
@ -62,38 +62,8 @@ public sealed class ContentText : IContent
|
||||
/// 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 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; }
|
||||
|
||||
/// <summary>
|
||||
/// What the provider said the answer itself cost, where it said anything.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Stored, kept, and dropped together with ReportedPromptTokens, for the reasons given there.
|
||||
/// </remarks>
|
||||
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);
|
||||
public ReportedTokenUsage? ReportedUsage { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public ToolRuntimeStatus ToolRuntimeStatus { get; set; } = new();
|
||||
@ -141,6 +111,29 @@ public sealed class ContentText : IContent
|
||||
/// </remarks>
|
||||
public void EndToolRun() => this.PendingToolConversation = [];
|
||||
|
||||
/// <summary>
|
||||
/// Keeps what the provider says the request behind this answer cost.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The one place where a stream's usage becomes part of the answer, for every path which writes
|
||||
/// one. It arrives on one line of the stream, usually the last one, and only where the provider
|
||||
/// reports it at all -- so a usage which states nothing leaves what was reported before alone.
|
||||
/// </remarks>
|
||||
/// <param name="usage">What the current chunk of the stream says, which is mostly nothing.</param>
|
||||
/// <param name="modelId">The model the request went to.</param>
|
||||
public void RecordReportedUsage(TokenUsage usage, string modelId)
|
||||
{
|
||||
if (!usage.IsKnown)
|
||||
return;
|
||||
|
||||
this.ReportedUsage = new()
|
||||
{
|
||||
PromptTokens = usage.PromptTokens,
|
||||
CompletionTokens = usage.CompletionTokens,
|
||||
ModelId = modelId,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastUserPrompt, ChatThread? chatThread, CancellationToken token = default)
|
||||
{
|
||||
@ -230,17 +223,8 @@ 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;
|
||||
}
|
||||
// Keep what the provider says the request cost:
|
||||
this.RecordReportedUsage(contentStreamChunk.Usage, chatModel.Id);
|
||||
|
||||
// Notify the UI that the content has changed,
|
||||
// depending on the energy saving mode:
|
||||
|
||||
45
app/MindWork AI Studio/Chat/ReportedTokenUsage.cs
Normal file
45
app/MindWork AI Studio/Chat/ReportedTokenUsage.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using AIStudio.Provider;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// What a provider said the request behind one answer cost, as it is stored on that answer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The numbers and the model they were charged for travel as one value. Each of them is meaningless
|
||||
/// without the others, and loose fields on the answer could be set, cleared, or copied apart.
|
||||
///
|
||||
/// Plain numbers rather than a TokenUsage: that type only ever comes out of its own factory, which
|
||||
/// is what keeps an impossible usage from existing, while a stored value has to be readable back by
|
||||
/// the serializer. ToTokenUsage is the way back, and it treats a chat file somebody edited by hand
|
||||
/// the same way the reading side treats a provider's JSON.
|
||||
/// </remarks>
|
||||
public sealed record ReportedTokenUsage
|
||||
{
|
||||
/// <summary>
|
||||
/// What everything sent to the model cost.
|
||||
/// </summary>
|
||||
public int PromptTokens { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// What the model wrote in answer.
|
||||
/// </summary>
|
||||
public int CompletionTokens { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Which model the numbers 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 ModelId { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// States the stored numbers as a usage again.
|
||||
/// </summary>
|
||||
/// <returns>The usage, or TokenUsage.UNKNOWN when the stored numbers state nothing usable.</returns>
|
||||
public TokenUsage ToTokenUsage() => TokenUsage.OfReported(this.PromptTokens, this.CompletionTokens);
|
||||
}
|
||||
@ -1650,10 +1650,11 @@ public partial class ChatComponent : MSGComponentBase
|
||||
{
|
||||
for (var index = thread.Blocks.Count - 1; index >= 0; index--)
|
||||
{
|
||||
if (thread.Blocks[index].Content is not ContentText { IsStreaming: false } text)
|
||||
if (thread.Blocks[index].Content is not ContentText { IsStreaming: false, ReportedUsage: { } reported })
|
||||
continue;
|
||||
|
||||
if (!text.ReportedTokens.IsKnown)
|
||||
var usage = reported.ToTokenUsage();
|
||||
if (!usage.IsKnown)
|
||||
continue;
|
||||
|
||||
//
|
||||
@ -1662,8 +1663,8 @@ public partial class ChatComponent : MSGComponentBase
|
||||
// answers of that same other model, so the search ends here and the estimate takes
|
||||
// over until this model has answered once.
|
||||
//
|
||||
return string.Equals(text.ReportedForModel, model.Id, StringComparison.Ordinal)
|
||||
? text.ReportedTokens
|
||||
return string.Equals(reported.ModelId, model.Id, StringComparison.Ordinal)
|
||||
? usage
|
||||
: TokenUsage.UNKNOWN;
|
||||
}
|
||||
|
||||
|
||||
@ -455,18 +455,7 @@ public sealed class AIJobService(SettingsManager settingsManager, MessageBus mes
|
||||
aiText.IsStreaming = true;
|
||||
aiText.Text += contentStreamChunk;
|
||||
aiText.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 a provider reports it at all -- so a chunk
|
||||
// without it leaves what was reported before alone.
|
||||
//
|
||||
if (contentStreamChunk.Usage.IsKnown)
|
||||
{
|
||||
aiText.ReportedPromptTokens = contentStreamChunk.Usage.PromptTokens;
|
||||
aiText.ReportedCompletionTokens = contentStreamChunk.Usage.CompletionTokens;
|
||||
aiText.ReportedForModel = state.ChatGenerationRequest.ProviderSettings.Model.Id;
|
||||
}
|
||||
aiText.RecordReportedUsage(contentStreamChunk.Usage, state.ChatGenerationRequest.ProviderSettings.Model.Id);
|
||||
|
||||
if (state.Snapshot.Status is not AIJobStatus.RUNNING)
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user