mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-14 10:33:48 +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
@@ -1037,6 +1037,20 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
// The line stating what the request cost carries no content of its own: providers
|
||||
// send it as the last line of the stream, with no choices at all. It is handled
|
||||
// before the check below, which would otherwise drop it as an empty response.
|
||||
//
|
||||
if (providerResponse.ContainsUsage())
|
||||
{
|
||||
yield return providerResponse.ContainsContent()
|
||||
? providerResponse.GetContent() with { Usage = providerResponse.GetUsage() }
|
||||
: new(string.Empty, [], providerResponse.GetUsage());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty responses:
|
||||
if (!providerResponse.ContainsContent())
|
||||
continue;
|
||||
|
||||
@@ -3,9 +3,15 @@ namespace AIStudio.Provider;
|
||||
/// <summary>
|
||||
/// A chunk of content from a content stream, along with its associated sources.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The usage rides along on the chunk rather than being reported next to the stream, because a
|
||||
/// provider states it as one more line of that same stream. It is unknown on every chunk but the
|
||||
/// one which carries it, and unknown on all of them at the providers which report nothing.
|
||||
/// </remarks>
|
||||
/// <param name="Content">The text content of the chunk.</param>
|
||||
/// <param name="Sources">The list of sources associated with the chunk.</param>
|
||||
public sealed record ContentStreamChunk(string Content, IList<ISource> Sources)
|
||||
/// <param name="Usage">What the provider said the request cost, where it said anything.</param>
|
||||
public sealed record ContentStreamChunk(string Content, IList<ISource> Sources, TokenUsage Usage = default)
|
||||
{
|
||||
/// <summary>
|
||||
/// Implicit conversion to string.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using AIStudio.Provider.OpenAI;
|
||||
|
||||
namespace AIStudio.Provider.Fireworks;
|
||||
|
||||
/// <summary>
|
||||
@@ -16,6 +18,22 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint
|
||||
/// <inheritdoc />
|
||||
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, []);
|
||||
|
||||
/// <summary>
|
||||
/// What Fireworks says the request cost, on the one line which carries it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The same block the OpenAI chat completion API sends, because this is that wire format.
|
||||
/// Not a positional parameter: the struct is built from JSON, and a further parameter would
|
||||
/// only be a value nobody passes.
|
||||
/// </remarks>
|
||||
public ChatCompletionUsage? Usage { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ContainsUsage() => this.GetUsage().IsKnown;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TokenUsage GetUsage() => this.Usage is null ? TokenUsage.UNKNOWN : TokenUsage.OfReported(this.Usage.PromptTokens, this.Usage.CompletionTokens);
|
||||
|
||||
#region Implementation of IAnnotationStreamLine
|
||||
|
||||
//
|
||||
|
||||
@@ -16,4 +16,20 @@ public interface IResponseStreamLine : IAnnotationStreamLine
|
||||
/// </summary>
|
||||
/// <returns>The content of the response line.</returns>
|
||||
public ContentStreamChunk GetContent();
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the response line states what the request cost.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Answered here for every wire format which says nothing about it, which is most of them: a
|
||||
/// provider who reports no usage is the normal case, not a gap somebody has to fill in.
|
||||
/// </remarks>
|
||||
/// <returns>True when the response line carries a token usage, false otherwise.</returns>
|
||||
public bool ContainsUsage() => false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets what the provider said the request cost.
|
||||
/// </summary>
|
||||
/// <returns>The usage, or <see cref="TokenUsage.UNKNOWN"/> when the line carries none.</returns>
|
||||
public TokenUsage GetUsage() => TokenUsage.UNKNOWN;
|
||||
}
|
||||
@@ -23,6 +23,17 @@ public record ChatCompletionAPIRequest(
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public bool? ParallelToolCalls { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Asks a streamed request to end with what it cost.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Derived rather than set, so that every provider which builds one of these asks for it
|
||||
/// without having to know that it exists. A request which is not streamed carries no such
|
||||
/// line, and then the block would only be a field the provider has to ignore.
|
||||
/// </remarks>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public ChatCompletionStreamOptions? StreamOptions => this.Stream ? ChatCompletionStreamOptions.INCLUDE_USAGE : null;
|
||||
|
||||
// Attention: The "required" modifier is not supported for [JsonExtensionData].
|
||||
[JsonExtensionData]
|
||||
|
||||
@@ -15,12 +15,30 @@ public record ChatCompletionDeltaStreamLine(string Id, string Object, uint Creat
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What the provider says the request cost, on the one line which carries it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not a positional parameter: every provider builds an empty line through the constructor
|
||||
/// above, and a further parameter would change all of those call sites for a value none of
|
||||
/// them has. Providers send this block only when the request asked for it, and then on a final
|
||||
/// line of its own which carries no choices -- which is why the usage is read apart from the
|
||||
/// content rather than next to it.
|
||||
/// </remarks>
|
||||
public ChatCompletionUsage? Usage { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ContainsContent() => this.Choices.Count > 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, []);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ContainsUsage() => this.GetUsage().IsKnown;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TokenUsage GetUsage() => this.Usage is null ? TokenUsage.UNKNOWN : TokenUsage.OfReported(this.Usage.PromptTokens, this.Usage.CompletionTokens);
|
||||
|
||||
#region Implementation of IAnnotationStreamLine
|
||||
|
||||
//
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// What a streamed chat completion should report beyond its content.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An OpenAI-compatible provider says nothing about what a streamed request cost unless it is asked
|
||||
/// to. Without this block, the stream simply ends and the only token number anybody ever sees is
|
||||
/// the one AI Studio estimated for itself.
|
||||
/// </remarks>
|
||||
/// <param name="IncludeUsage">Whether the stream should end with a line stating the token usage.</param>
|
||||
public sealed record ChatCompletionStreamOptions(bool IncludeUsage)
|
||||
{
|
||||
/// <summary>
|
||||
/// Asks for the usage line.
|
||||
/// </summary>
|
||||
public static readonly ChatCompletionStreamOptions INCLUDE_USAGE = new(true);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
/// <summary>
|
||||
/// What an OpenAI-compatible provider reports a chat completion cost.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every number is optional because this is somebody else's JSON: the block arrives only when the
|
||||
/// request asked for it, and the providers which follow the shape loosely leave fields out. Reading
|
||||
/// it is one thing, believing it another -- <see cref="TokenUsage.OfReported"/> decides that.
|
||||
/// </remarks>
|
||||
public sealed record ChatCompletionUsage
|
||||
{
|
||||
/// <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>
|
||||
/// What the provider says both of them add up to. Read but not relied upon: it is the sum of
|
||||
/// the other two wherever a provider fills all three, and this way a provider which sends only
|
||||
/// this one is not a reason to throw the other numbers away.
|
||||
/// </summary>
|
||||
public int? TotalTokens { get; init; }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using AIStudio.Provider.OpenAI;
|
||||
|
||||
namespace AIStudio.Provider.Perplexity;
|
||||
|
||||
/// <summary>
|
||||
@@ -16,6 +18,22 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint
|
||||
|
||||
/// <inheritdoc />
|
||||
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, this.GetSources());
|
||||
|
||||
/// <summary>
|
||||
/// What Perplexity says the request cost, on the one line which carries it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The same block the OpenAI chat completion API sends, because this is that wire format.
|
||||
/// Not a positional parameter: the struct is built from JSON, and a further parameter would
|
||||
/// only be a value nobody passes.
|
||||
/// </remarks>
|
||||
public ChatCompletionUsage? Usage { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ContainsUsage() => this.GetUsage().IsKnown;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TokenUsage GetUsage() => this.Usage is null ? TokenUsage.UNKNOWN : TokenUsage.OfReported(this.Usage.PromptTokens, this.Usage.CompletionTokens);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ContainsSources() => this != default && this.SearchResults.Count > 0;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
/// <summary>
|
||||
/// What a provider said one request actually cost, in tokens.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The counterpart to what the app counts for itself: the app estimates what the next request will
|
||||
/// cost, while this is what the provider charged for the last one. Two different statements, and
|
||||
/// this one is the only exact one of the two.
|
||||
///
|
||||
/// Nothing here says "unknown" with a zero. The default value of this type is unknown, which is the
|
||||
/// right answer for every provider which reports nothing, and a counted request can never cost zero
|
||||
/// prompt tokens because the factory below refuses to build one.
|
||||
/// </remarks>
|
||||
public readonly record struct TokenUsage
|
||||
{
|
||||
/// <summary>
|
||||
/// The usage of a request nobody reported anything about.
|
||||
/// </summary>
|
||||
public static readonly TokenUsage UNKNOWN = new();
|
||||
|
||||
/// <summary>
|
||||
/// Whether a provider reported anything at all. When false, the numbers are meaningless.
|
||||
/// </summary>
|
||||
public bool IsKnown { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// What everything sent to the model cost: the conversation so far, its attachments, the system
|
||||
/// prompt, and whatever tools were offered.
|
||||
/// </summary>
|
||||
public int PromptTokens { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// What the model wrote in answer.
|
||||
/// </summary>
|
||||
public int CompletionTokens { get; private init; }
|
||||
|
||||
/// <summary>
|
||||
/// What the whole exchange cost, which is what the next request carries as its history.
|
||||
/// </summary>
|
||||
public int TotalTokens => this.PromptTokens + this.CompletionTokens;
|
||||
|
||||
/// <summary>
|
||||
/// States what a provider reported.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A completion of zero tokens is a real answer: a model which was cut off before writing
|
||||
/// anything still cost its prompt. A prompt of zero is not, because there is no request
|
||||
/// without one, and a provider sending it means we read the wrong field.
|
||||
/// </remarks>
|
||||
/// <param name="promptTokens">What the request carried. Has to be greater than zero.</param>
|
||||
/// <param name="completionTokens">What the answer cost. Zero or more.</param>
|
||||
/// <returns>The usage.</returns>
|
||||
public static TokenUsage Of(int promptTokens, int completionTokens)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(promptTokens);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(completionTokens);
|
||||
|
||||
return new()
|
||||
{
|
||||
IsKnown = true,
|
||||
PromptTokens = promptTokens,
|
||||
CompletionTokens = completionTokens,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// States what a provider reported, or unknown when it reported nothing usable.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For the reading side, where the numbers come out of somebody else's JSON: a missing field, a
|
||||
/// null, or a zero prompt all mean the same thing there, and none of them is worth an exception.
|
||||
/// </remarks>
|
||||
/// <param name="promptTokens">What the request carried, as the provider stated it.</param>
|
||||
/// <param name="completionTokens">What the answer cost, as the provider stated it.</param>
|
||||
/// <returns>The usage, or <see cref="UNKNOWN"/>.</returns>
|
||||
public static TokenUsage OfReported(int? promptTokens, int? completionTokens) =>
|
||||
promptTokens is > 0
|
||||
? Of(promptTokens.Value, completionTokens is > 0 ? completionTokens.Value : 0)
|
||||
: UNKNOWN;
|
||||
}
|
||||
Reference in new issue
Block a user