diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs
index 5fb3e318..0cac137d 100644
--- a/app/MindWork AI Studio/Provider/BaseProvider.cs
+++ b/app/MindWork AI Studio/Provider/BaseProvider.cs
@@ -1123,11 +1123,12 @@ public abstract class BaseProvider : IProvider, ISecretId
// 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())
+ var usage = providerResponse.GetUsage();
+ if (usage.IsKnown)
{
yield return providerResponse.ContainsContent()
- ? providerResponse.GetContent() with { Usage = providerResponse.GetUsage() }
- : new(string.Empty, [], providerResponse.GetUsage());
+ ? providerResponse.GetContent() with { Usage = usage }
+ : new(string.Empty, [], usage);
continue;
}
diff --git a/app/MindWork AI Studio/Provider/Fireworks/ResponseStreamLine.cs b/app/MindWork AI Studio/Provider/Fireworks/ResponseStreamLine.cs
index b8524a4f..7f46c7ba 100644
--- a/app/MindWork AI Studio/Provider/Fireworks/ResponseStreamLine.cs
+++ b/app/MindWork AI Studio/Provider/Fireworks/ResponseStreamLine.cs
@@ -29,10 +29,7 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint
public ChatCompletionUsage? Usage { get; init; }
///
- public bool ContainsUsage() => this.GetUsage().IsKnown;
-
- ///
- public TokenUsage GetUsage() => this.Usage is null ? TokenUsage.UNKNOWN : TokenUsage.OfReported(this.Usage.PromptTokens, this.Usage.CompletionTokens);
+ public TokenUsage GetUsage() => this.Usage?.ToTokenUsage() ?? TokenUsage.UNKNOWN;
#region Implementation of IAnnotationStreamLine
diff --git a/app/MindWork AI Studio/Provider/IResponseStreamLine.cs b/app/MindWork AI Studio/Provider/IResponseStreamLine.cs
index e09e1e71..f26faa9c 100644
--- a/app/MindWork AI Studio/Provider/IResponseStreamLine.cs
+++ b/app/MindWork AI Studio/Provider/IResponseStreamLine.cs
@@ -18,18 +18,16 @@ public interface IResponseStreamLine : IAnnotationStreamLine
public ContentStreamChunk GetContent();
///
- /// Checks whether the response line states what the request cost.
+ /// Gets what the provider said the request cost.
///
///
/// 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.
+ ///
+ /// Unlike content and sources, there is no separate check for whether a line carries it. This
+ /// never fails on a line without one, and whether the answer means anything is what IsKnown of
+ /// the returned usage says.
///
- /// True when the response line carries a token usage, false otherwise.
- public bool ContainsUsage() => false;
-
- ///
- /// Gets what the provider said the request cost.
- ///
/// The usage, or TokenUsage.UNKNOWN when the line carries none.
public TokenUsage GetUsage() => TokenUsage.UNKNOWN;
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDeltaStreamLine.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDeltaStreamLine.cs
index dc697317..cdd1dfef 100644
--- a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDeltaStreamLine.cs
+++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionDeltaStreamLine.cs
@@ -34,10 +34,7 @@ public record ChatCompletionDeltaStreamLine(string Id, string Object, uint Creat
public ContentStreamChunk GetContent() => new(this.Choices[0].Delta.Content, []);
///
- public bool ContainsUsage() => this.GetUsage().IsKnown;
-
- ///
- public TokenUsage GetUsage() => this.Usage is null ? TokenUsage.UNKNOWN : TokenUsage.OfReported(this.Usage.PromptTokens, this.Usage.CompletionTokens);
+ public TokenUsage GetUsage() => this.Usage?.ToTokenUsage() ?? TokenUsage.UNKNOWN;
#region Implementation of IAnnotationStreamLine
diff --git a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionUsage.cs b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionUsage.cs
index fbfc99df..5108cf7f 100644
--- a/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionUsage.cs
+++ b/app/MindWork AI Studio/Provider/OpenAI/ChatCompletionUsage.cs
@@ -22,9 +22,12 @@ public sealed record ChatCompletionUsage
public int? CompletionTokens { get; init; }
///
- /// 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.
+ /// States what this block reports, as far as it can be believed.
///
- public int? TotalTokens { get; init; }
+ ///
+ /// The one way from the wire to a usage, shared by every stream line which carries this block,
+ /// so that what counts as believable is decided in a single place.
+ ///
+ /// The usage, or TokenUsage.UNKNOWN when the block states nothing usable.
+ public TokenUsage ToTokenUsage() => TokenUsage.OfReported(this.PromptTokens, this.CompletionTokens);
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Provider/Perplexity/ResponseStreamLine.cs b/app/MindWork AI Studio/Provider/Perplexity/ResponseStreamLine.cs
index 17c96ca2..e31b072b 100644
--- a/app/MindWork AI Studio/Provider/Perplexity/ResponseStreamLine.cs
+++ b/app/MindWork AI Studio/Provider/Perplexity/ResponseStreamLine.cs
@@ -30,10 +30,7 @@ public readonly record struct ResponseStreamLine(string Id, string Object, uint
public ChatCompletionUsage? Usage { get; init; }
///
- public bool ContainsUsage() => this.GetUsage().IsKnown;
-
- ///
- public TokenUsage GetUsage() => this.Usage is null ? TokenUsage.UNKNOWN : TokenUsage.OfReported(this.Usage.PromptTokens, this.Usage.CompletionTokens);
+ public TokenUsage GetUsage() => this.Usage?.ToTokenUsage() ?? TokenUsage.UNKNOWN;
///
public bool ContainsSources() => this != default && this.SearchResults.Count > 0;
diff --git a/app/Tests/Provider/ChatCompletionUsageTests.cs b/app/Tests/Provider/ChatCompletionUsageTests.cs
index ff0123fc..46e421ab 100644
--- a/app/Tests/Provider/ChatCompletionUsageTests.cs
+++ b/app/Tests/Provider/ChatCompletionUsageTests.cs
@@ -41,7 +41,7 @@ public sealed class ChatCompletionUsageTests
Assert.Multiple(() =>
{
- Assert.That(line!.ContainsUsage(), Is.True);
+ Assert.That(line!.GetUsage().IsKnown, Is.True);
Assert.That(line.GetUsage().PromptTokens, Is.EqualTo(1200));
Assert.That(line.GetUsage().CompletionTokens, Is.EqualTo(345));
Assert.That(line.GetUsage().TotalTokens, Is.EqualTo(1545));
@@ -61,8 +61,7 @@ public sealed class ChatCompletionUsageTests
Assert.Multiple(() =>
{
- Assert.That(line!.ContainsUsage(), Is.False);
- Assert.That(line.GetUsage().IsKnown, Is.False);
+ Assert.That(line!.GetUsage().IsKnown, Is.False);
Assert.That(line.ContainsContent(), Is.True);
});
}
@@ -103,7 +102,7 @@ public sealed class ChatCompletionUsageTests
{"id":"chatcmpl-1","object":"chat.completion.chunk","created":1,"model":"gpt-5","choices":[],"usage":{"prompt_tokens":0,"completion_tokens":0}}
""", ProviderJsonOptions.OPTIONS);
- Assert.That(line!.ContainsUsage(), Is.False);
+ Assert.That(line!.GetUsage().IsKnown, Is.False);
}
///
@@ -124,7 +123,7 @@ public sealed class ChatCompletionUsageTests
Assert.Multiple(() =>
{
- Assert.That(line!.ContainsUsage(), Is.True);
+ Assert.That(line!.GetUsage().IsKnown, Is.True);
Assert.That(line.GetUsage().TotalTokens, Is.EqualTo(133));
});
}