mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(usage): Claude 流式合并 message_start/delta usage,前端移动端缓存 token 合并展示
- Claude provider state 在 message_start 记录基础 usage,message_delta 合并 output_tokens - 前端 UsageRecordsTable 移动端缓存列改为 cache_creation + cache_read 合计展示
This commit is contained in:
@@ -20,6 +20,7 @@ pub struct ClaudeProviderState {
|
||||
model: Option<String>,
|
||||
started: bool,
|
||||
finished: bool,
|
||||
usage: Option<CanonicalUsage>,
|
||||
tool_calls: BTreeMap<usize, ClaudeProviderToolState>,
|
||||
}
|
||||
|
||||
@@ -73,6 +74,7 @@ impl ClaudeProviderState {
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned);
|
||||
self.merge_usage(canonical_usage_from_claude_usage(message.get("usage")));
|
||||
}
|
||||
self.ensure_started(report_context, &mut out);
|
||||
}
|
||||
@@ -303,7 +305,9 @@ impl ClaudeProviderState {
|
||||
model,
|
||||
event: CanonicalStreamEvent::Finish {
|
||||
finish_reason,
|
||||
usage: canonical_usage_from_claude_usage(event_object.get("usage")),
|
||||
usage: self.finish_usage(canonical_usage_from_claude_usage(
|
||||
event_object.get("usage"),
|
||||
)),
|
||||
},
|
||||
});
|
||||
self.finished = true;
|
||||
@@ -331,6 +335,19 @@ impl ClaudeProviderState {
|
||||
},
|
||||
}])
|
||||
}
|
||||
|
||||
fn merge_usage(&mut self, usage: Option<CanonicalUsage>) {
|
||||
let Some(usage) = usage else {
|
||||
return;
|
||||
};
|
||||
let current = self.usage.take().unwrap_or_default();
|
||||
self.usage = Some(merge_claude_usage(current, usage));
|
||||
}
|
||||
|
||||
fn finish_usage(&mut self, usage: Option<CanonicalUsage>) -> Option<CanonicalUsage> {
|
||||
self.merge_usage(usage);
|
||||
self.usage.take()
|
||||
}
|
||||
}
|
||||
|
||||
enum ClaudeOpenBlock {
|
||||
@@ -771,6 +788,34 @@ impl ClaudeClientEmitter {
|
||||
)?);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn merge_claude_usage(mut current: CanonicalUsage, next: CanonicalUsage) -> CanonicalUsage {
|
||||
if next.input_tokens > 0 {
|
||||
current.input_tokens = next.input_tokens;
|
||||
}
|
||||
if next.output_tokens > 0 {
|
||||
current.output_tokens = next.output_tokens;
|
||||
}
|
||||
if next.cache_creation_tokens > 0 {
|
||||
current.cache_creation_tokens = next.cache_creation_tokens;
|
||||
}
|
||||
if next.cache_creation_ephemeral_5m_tokens > 0 {
|
||||
current.cache_creation_ephemeral_5m_tokens = next.cache_creation_ephemeral_5m_tokens;
|
||||
}
|
||||
if next.cache_creation_ephemeral_1h_tokens > 0 {
|
||||
current.cache_creation_ephemeral_1h_tokens = next.cache_creation_ephemeral_1h_tokens;
|
||||
}
|
||||
if next.cache_read_tokens > 0 {
|
||||
current.cache_read_tokens = next.cache_read_tokens;
|
||||
}
|
||||
current.total_tokens = current
|
||||
.input_tokens
|
||||
.saturating_add(current.output_tokens)
|
||||
.saturating_add(current.cache_creation_tokens)
|
||||
.saturating_add(current.cache_read_tokens);
|
||||
current
|
||||
}
|
||||
|
||||
fn canonical_content_part_from_claude_block(
|
||||
@@ -945,6 +990,58 @@ mod tests {
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_provider_state_merges_start_and_delta_usage() {
|
||||
let mut state = ClaudeProviderState::default();
|
||||
let report_context = json!({});
|
||||
let _ = state
|
||||
.push_line(
|
||||
&report_context,
|
||||
data_line(json!({
|
||||
"type": "message_start",
|
||||
"message": {
|
||||
"id": "msg_123",
|
||||
"model": "claude-sonnet-4-5",
|
||||
"usage": {
|
||||
"input_tokens": 5,
|
||||
"cache_creation_input_tokens": 59_573,
|
||||
"cache_read_input_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
},
|
||||
},
|
||||
})),
|
||||
)
|
||||
.expect("message_start should parse");
|
||||
let frames = state
|
||||
.push_line(
|
||||
&report_context,
|
||||
data_line(json!({
|
||||
"type": "message_delta",
|
||||
"delta": {
|
||||
"stop_reason": "end_turn",
|
||||
},
|
||||
"usage": {
|
||||
"output_tokens": 162,
|
||||
},
|
||||
})),
|
||||
)
|
||||
.expect("message_delta should parse");
|
||||
|
||||
assert!(frames.iter().any(|frame| matches!(
|
||||
frame.event,
|
||||
CanonicalStreamEvent::Finish {
|
||||
usage: Some(CanonicalUsage {
|
||||
input_tokens: 5,
|
||||
output_tokens: 162,
|
||||
cache_creation_tokens: 59_573,
|
||||
cache_read_tokens: 0,
|
||||
..
|
||||
}),
|
||||
..
|
||||
}
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_client_emitter_preserves_tool_identity_and_emits_thinking_blocks() {
|
||||
let mut emitter = ClaudeClientEmitter::default();
|
||||
|
||||
@@ -569,7 +569,7 @@
|
||||
<span>{{ formatTokens(record.output_tokens || 0) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 text-muted-foreground">
|
||||
<span :class="hasPositiveTokens(record.cache_read_input_tokens) ? 'text-foreground/70' : ''">{{ formatOptionalTokens(record.cache_read_input_tokens) }}</span>
|
||||
<span :class="hasPositiveTokens(getRecordCacheTokens(record)) ? 'text-foreground/70' : ''">{{ formatOptionalTokens(getRecordCacheTokens(record)) }}</span>
|
||||
<span>/</span>
|
||||
<span>-</span>
|
||||
</div>
|
||||
@@ -689,7 +689,7 @@ import {
|
||||
import { RefreshCcw, Search } from 'lucide-vue-next'
|
||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||
import { formatDateTime } from '../composables'
|
||||
import { getEffectiveInputTokens } from '../token-normalization'
|
||||
import { getCacheCreationTokens, getEffectiveInputTokens } from '../token-normalization'
|
||||
import {
|
||||
formatUsageStreamLabel,
|
||||
isUsageRecordFailed,
|
||||
@@ -825,6 +825,10 @@ function getRecordEffectiveInputTokens(record: UsageRecord): number {
|
||||
return getEffectiveInputTokens(record)
|
||||
}
|
||||
|
||||
function getRecordCacheTokens(record: UsageRecord): number {
|
||||
return getCacheCreationTokens(record) + (record.cache_read_input_tokens || 0)
|
||||
}
|
||||
|
||||
function hasPositiveTokens(value: number | null | undefined): boolean {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user