Added the exact token count where the provider reports it

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
j-erlerandClaude Opus 5 committed 2026-09-20 17:42:29 +02:00
1 parent c95cc5bacc
commit 0aa3b23b81
21 files changed
+598 -7

No files matched your search

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