Skip to content

Commit 52f5bb5

Browse files
7418claude
andcommitted
feat: add token usage statistics page in Settings
New "Usage" tab in Settings showing token consumption analytics: - **Data cards**: Total Tokens, Total Cost, Sessions, Cache Hit Rate - **Bar chart**: Daily token usage with model/provider color grouping using recharts v3, with custom tooltip and continuous date x-axis - **Provider tracking**: Each session now records the active provider name (e.g. "Kimi Coding Plan") so the chart distinguishes usage across different providers, not just model names Backend changes (db.ts): - Added `provider_name` column to chat_sessions with auto-migration - Added `getTokenUsageStats(days)` with summary + daily aggregation - SQL uses `json_valid()` guard to prevent malformed JSON crashes - Fixed off-by-one: 7D now shows exactly 7 calendar days, not 8 - Provider-qualified grouping: shows provider name when set, plain model name when using direct Anthropic API changes (route.ts): - Persists effectiveModel and activeProvider name on every message - Captures model from SDK init event for accurate tracking - New GET /api/usage/stats endpoint Frontend fixes: - SettingsLayout: replaced useEffect+setState with useSyncExternalStore to fix react-hooks/set-state-in-effect lint error - UsageStatsSection: AbortController prevents request race on rapid range switching; cache hit rate formula corrected to cache_read/(cache_read+input); date gaps filled with zeros for continuous x-axis - Full i18n support (en + zh) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent edd55a9 commit 52f5bb5

File tree

10 files changed

+821
-31
lines changed

10 files changed

+821
-31
lines changed

package-lock.json

Lines changed: 185 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codepilot",
3-
"version": "0.13.2",
3+
"version": "0.14.0",
44
"private": true,
55
"author": {
66
"name": "op7418",
@@ -45,6 +45,7 @@
4545
"react-dom": "19.2.3",
4646
"react-markdown": "^10.1.0",
4747
"react-syntax-highlighter": "^16.1.0",
48+
"recharts": "^3.7.0",
4849
"rehype-raw": "^7.0.0",
4950
"remark-gfm": "^4.0.1",
5051
"shiki": "^3.22.0",

src/app/api/chat/route.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { NextRequest } from 'next/server';
22
import { streamClaude } from '@/lib/claude-client';
3-
import { addMessage, getSession, updateSessionTitle, updateSdkSessionId, getSetting } from '@/lib/db';
3+
import { addMessage, getSession, updateSessionTitle, updateSdkSessionId, updateSessionModel, updateSessionProvider, getSetting, getActiveProvider } from '@/lib/db';
44
import type { SendMessageRequest, SSEEvent, TokenUsage, MessageContentBlock, FileAttachment } from '@/types';
55
import fs from 'fs';
66
import path from 'path';
@@ -57,6 +57,17 @@ export async function POST(request: NextRequest) {
5757
// Determine model: request override > session model > default setting
5858
const effectiveModel = model || session.model || getSetting('default_model') || undefined;
5959

60+
// Persist model and provider to session so usage stats can group by model+provider.
61+
// This runs on every message but the DB writes are cheap (single UPDATE by PK).
62+
if (effectiveModel && effectiveModel !== session.model) {
63+
updateSessionModel(session_id, effectiveModel);
64+
}
65+
const activeProvider = getActiveProvider();
66+
const providerName = activeProvider?.name || '';
67+
if (providerName !== (session.provider_name || '')) {
68+
updateSessionProvider(session_id, providerName);
69+
}
70+
6071
// Determine permission mode from chat mode: code → acceptEdits, plan → plan, ask → default (no tools)
6172
const effectiveMode = mode || session.mode || 'code';
6273
let permissionMode: string;
@@ -185,12 +196,15 @@ async function collectStreamResponse(stream: ReadableStream<string>, sessionId:
185196
// skip malformed tool_result data
186197
}
187198
} else if (event.type === 'status') {
188-
// Capture SDK session_id from init event and persist it
199+
// Capture SDK session_id and model from init event and persist them
189200
try {
190201
const statusData = JSON.parse(event.data);
191202
if (statusData.session_id) {
192203
updateSdkSessionId(sessionId, statusData.session_id);
193204
}
205+
if (statusData.model) {
206+
updateSessionModel(sessionId, statusData.model);
207+
}
194208
} catch {
195209
// skip malformed status data
196210
}

src/app/api/usage/stats/route.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { NextRequest } from 'next/server';
2+
import { getTokenUsageStats } from '@/lib/db';
3+
4+
export const runtime = 'nodejs';
5+
export const dynamic = 'force-dynamic';
6+
7+
export async function GET(request: NextRequest) {
8+
try {
9+
const searchParams = request.nextUrl.searchParams;
10+
const daysParam = searchParams.get('days');
11+
const days = daysParam ? Math.min(Math.max(parseInt(daysParam, 10) || 30, 1), 365) : 30;
12+
13+
const stats = getTokenUsageStats(days);
14+
return Response.json(stats);
15+
} catch (error) {
16+
const message = error instanceof Error ? error.message : 'Failed to fetch usage stats';
17+
return Response.json({ error: message }, { status: 500 });
18+
}
19+
}

0 commit comments

Comments
 (0)