Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ Write each change in both `### English` and `### 中文` under `## Unreleased`.

### English

- Show consumed points under the Tokens column in request history when a provider reports them; keep writing the value on the request log row, and fall back to the request-detail table for rows that only stored it there

### 中文

- 请求历史上游若回报消耗点数,会在 Tokens 列下方以绿色小字展示;继续写入请求日志主表对应字段,并对仅记在详情表的历史行做回退读取

## 0.5.4 - 2026-09-16

### English
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export const messages: Record<Lang, Dict> = {
logsColTokens: 'Tokens',
logsTokensIn: 'in',
logsTokensOut: 'out',
logsTokensPoints: '{value} pts',
logsColStream: 'Mode',
logsStreamYes: 'stream',
logsStreamNo: 'sync',
Expand Down Expand Up @@ -810,6 +811,7 @@ export const messages: Record<Lang, Dict> = {
logsColTokens: 'Tokens',
logsTokensIn: '输入',
logsTokensOut: '输出',
logsTokensPoints: '{value} 点',
logsColStream: '模式',
logsStreamYes: '流式',
logsStreamNo: '同步',
Expand Down
45 changes: 39 additions & 6 deletions frontend/src/pages/LogsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,37 @@ function formatTime(value?: string | null, lang: 'en' | 'zh' = 'zh') {
}).format(date)
}

function TokenSplit({ log, inLabel, outLabel }: { log: RequestLog; inLabel: string; outLabel: string }) {
function TokenSplit({
log,
inLabel,
outLabel,
pointsLabel,
}: {
log: RequestLog
inLabel: string
outLabel: string
pointsLabel?: (value: string) => string
}) {
const prompt = log.prompt_tokens
const completion = log.completion_tokens
if (prompt == null && completion == null) {
const credit = log.credits ?? log.usage_detail?.credit
const creditText = credit != null && Number.isFinite(credit) ? formatCredit(credit) : null
if (prompt == null && completion == null && creditText == null) {
return <span className="mono text-xs text-muted">—</span>
}
return (
<div className="leading-4">
<div className="mono text-xs">{prompt ?? 0} / {completion ?? 0}</div>
<div className="mt-0.5 text-[10px] text-muted">{inLabel} / {outLabel}</div>
{prompt != null || completion != null ? (
<>
<div className="mono text-xs">{prompt ?? 0} / {completion ?? 0}</div>
<div className="mt-0.5 text-[10px] text-muted">{inLabel} / {outLabel}</div>
</>
) : null}
{creditText != null ? (
<div className={`mono text-[10px] text-success ${prompt != null || completion != null ? 'mt-0.5' : ''}`}>
{pointsLabel ? pointsLabel(creditText) : creditText}
</div>
) : null}
</div>
)
}
Expand Down Expand Up @@ -724,7 +745,14 @@ export function LogsPage() {
<Table.Cell><span className="text-xs text-muted">{item.stream ? t('logsStreamYes') : t('logsStreamNo')}</span></Table.Cell>
<Table.Cell><span className="mono text-xs">{formatLatency(item.latency_ms)}</span></Table.Cell>
<Table.Cell><span className="mono text-xs">{formatLatency(item.ttfb_ms)}</span></Table.Cell>
<Table.Cell><TokenSplit log={item} inLabel={t('logsTokensIn')} outLabel={t('logsTokensOut')} /></Table.Cell>
<Table.Cell>
<TokenSplit
log={item}
inLabel={t('logsTokensIn')}
outLabel={t('logsTokensOut')}
pointsLabel={(value) => t('logsTokensPoints', { value })}
/>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
Expand Down Expand Up @@ -939,7 +967,12 @@ export function LogsPage() {
<dt className="text-[11px] text-muted">{t('logsColTokens')}</dt>
<dd className="mt-1">
{selected ? (
<TokenSplit log={selected} inLabel={t('logsTokensIn')} outLabel={t('logsTokensOut')} />
<TokenSplit
log={selected}
inLabel={t('logsTokensIn')}
outLabel={t('logsTokensOut')}
pointsLabel={(value) => t('logsTokensPoints', { value })}
/>
) : '—'}
</dd>
</div>
Expand Down
26 changes: 14 additions & 12 deletions internal/accounts/request_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,12 +309,13 @@ func (s *Store) ListRequestLogs(ctx context.Context, filter RequestLogFilter) (R
return RequestLogList{}, fmt.Errorf("count request logs: %w", err)
}

query := `
SELECT id, created_at, finished_at, stream, status, requested_model, mapped_model, account_id,
COALESCE(NULLIF(provider, ''), (SELECT provider FROM accounts WHERE accounts.id = request_logs.account_id), ''), routing,
prompt_tokens, completion_tokens, cache_read_tokens, cache_write_tokens, usage_source, credits,
latency_ms, ttfb_ms, error_kind, error_code, error_message, attempt_count, message_count, empty_message_indexes, message_roles
FROM request_logs` + where + ` ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?`
query := `
SELECT id, created_at, finished_at, stream, status, requested_model, mapped_model, account_id,
COALESCE(NULLIF(provider, ''), (SELECT provider FROM accounts WHERE accounts.id = request_logs.account_id), ''), routing,
prompt_tokens, completion_tokens, cache_read_tokens, cache_write_tokens, usage_source,
COALESCE(credits, (SELECT credit FROM request_usage_details WHERE request_usage_details.request_id = request_logs.id)),
latency_ms, ttfb_ms, error_kind, error_code, error_message, attempt_count, message_count, empty_message_indexes, message_roles
FROM request_logs` + where + ` ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?`
args = append(args, limit, offset)
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
Expand Down Expand Up @@ -646,12 +647,13 @@ func percentileNearestRank(sorted []int, percentile int) int {
}

func (s *Store) GetRequestLog(ctx context.Context, id string) (RequestLog, error) {
row := s.db.QueryRowContext(ctx, `
SELECT id, created_at, finished_at, stream, status, requested_model, mapped_model, account_id,
COALESCE(NULLIF(provider, ''), (SELECT provider FROM accounts WHERE accounts.id = request_logs.account_id), ''), routing,
prompt_tokens, completion_tokens, cache_read_tokens, cache_write_tokens, usage_source, credits,
latency_ms, ttfb_ms, error_kind, error_code, error_message, attempt_count, message_count, empty_message_indexes, message_roles
FROM request_logs WHERE id = ?`, strings.TrimSpace(id))
row := s.db.QueryRowContext(ctx, `
SELECT id, created_at, finished_at, stream, status, requested_model, mapped_model, account_id,
COALESCE(NULLIF(provider, ''), (SELECT provider FROM accounts WHERE accounts.id = request_logs.account_id), ''), routing,
prompt_tokens, completion_tokens, cache_read_tokens, cache_write_tokens, usage_source,
COALESCE(credits, (SELECT credit FROM request_usage_details WHERE request_usage_details.request_id = request_logs.id)),
latency_ms, ttfb_ms, error_kind, error_code, error_message, attempt_count, message_count, empty_message_indexes, message_roles
FROM request_logs WHERE id = ?`, strings.TrimSpace(id))
log, err := scanRequestLog(row)
if errors.Is(err, sql.ErrNoRows) {
return RequestLog{}, ErrRequestLogNotFound
Expand Down
15 changes: 9 additions & 6 deletions internal/accounts/request_logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,15 @@ func TestRequestLogsInsertListGetAndPurge(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if list.Total != 1 || len(list.Items) != 1 || list.Items[0].Status != RequestStatusOK || list.Items[0].Provider != "workbuddy" {
t.Fatalf("list = %+v", list)
}
if list.Items[0].UsageDetail != nil {
t.Fatalf("list should not carry usage detail: %+v", list.Items[0].UsageDetail)
}
if list.Total != 1 || len(list.Items) != 1 || list.Items[0].Status != RequestStatusOK || list.Items[0].Provider != "workbuddy" {
t.Fatalf("list = %+v", list)
}
if list.Items[0].Credits == nil || *list.Items[0].Credits != 0.75 {
t.Fatalf("list credits fallback = %+v", list.Items[0].Credits)
}
if list.Items[0].UsageDetail != nil {
t.Fatalf("list should not carry usage detail: %+v", list.Items[0].UsageDetail)
}

got, err := store.GetRequestLog(ctx, parentID)
if err != nil {
Expand Down
45 changes: 22 additions & 23 deletions internal/api/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -1076,39 +1076,38 @@ func (s *Server) finishRequestLog(requestID string, started time.Time, req trans
} else if !req.Stream && entry.LatencyMs != nil {
entry.TTFBMs = entry.LatencyMs
}
if stats != nil {
entry.PromptTokens = stats.PromptTokens
entry.CompletionTokens = stats.CompletionTokens
entry.CacheReadTokens = stats.CacheReadTokens
entry.CacheWriteTokens = stats.CacheWriteTokens
entry.UsageSource = stats.UsageSource
if stats.Model != "" {
entry.MappedModel = stats.Model
if stats != nil {
entry.PromptTokens = stats.PromptTokens
entry.CompletionTokens = stats.CompletionTokens
entry.CacheReadTokens = stats.CacheReadTokens
entry.CacheWriteTokens = stats.CacheWriteTokens
entry.UsageSource = stats.UsageSource
consumed := stats.ConsumedCredits
if consumed == nil {
consumed = stats.Credits
}
entry.Credits = consumed
if stats.Model != "" {
entry.MappedModel = stats.Model
}
}
}
if err != nil {
classified := classifyAPIError(err)
entry.ErrorKind = classified.Kind
entry.ErrorCode = classified.Code
entry.ErrorMessage = classified.Message
}
s.recorder.Finish(entry)
if stats != nil {
consumed := stats.ConsumedCredits
if consumed == nil {
consumed = stats.Credits
if err != nil {
classified := classifyAPIError(err)
entry.ErrorKind = classified.Kind
entry.ErrorCode = classified.Code
entry.ErrorMessage = classified.Message
}
if consumed != nil {
s.recorder.Finish(entry)
if stats != nil && entry.Credits != nil {
s.recorder.UsageDetail(accounts.RequestUsageDetail{
RequestID: requestID,
CreatedAt: started,
Provider: provider,
Credit: consumed,
Credit: entry.Credits,
Unit: "credits",
})
}
}
}

func (s *Server) recordStreamDiagnostic(requestID string, response *http.Response, started time.Time, stats streamRelayStats, relayErr, contextErr error) {
if s.recorder == nil || requestID == "" {
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion internal/webui/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="/assets/index-DJCKOPb-.js"></script>
<script type="module" crossorigin src="/assets/index-CwU6lAfD.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DeIyZIPl.css">
</head>
<body>
Expand Down
Loading