feat: add token usage tracking and leaderboard
- Add token_usage table with composite unique index for claw_id + date - Add API endpoints: POST /token, GET /token/leaderboard, GET /token/stats - Add TokenLeaderboard component with daily/total period toggle - Add CLI commands: `token` and `stats` for reporting and viewing usage - Add Redis caching for leaderboard with 1-minute TTL - Add shared utilities: authenticateRequest, getTodayDateString - Use UPSERT pattern for atomic token updates - Add i18n translations (en/zh) for new UI elements
This commit is contained in:
55
app/api/v1/token/route.ts
Normal file
55
app/api/v1/token/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { db } from "@/lib/db";
|
||||
import { invalidateTokenLeaderboardCache } from "@/lib/redis";
|
||||
import { authenticateRequest, getTodayDateString } from "@/lib/utils";
|
||||
import { tokenSchema } from "@/lib/validators/schemas";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const auth = await authenticateRequest(req);
|
||||
if (auth instanceof NextResponse) {
|
||||
return auth;
|
||||
}
|
||||
const { claw } = auth;
|
||||
|
||||
const body = await req.json();
|
||||
const parsed = tokenSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(
|
||||
{ error: "Validation failed", details: parsed.error.flatten() },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { inputTokens, outputTokens, date } = parsed.data;
|
||||
const targetDate = date || getTodayDateString();
|
||||
|
||||
// Use UPSERT (INSERT ... ON DUPLICATE KEY UPDATE) for atomic operation
|
||||
await db.execute(sql`
|
||||
INSERT INTO token_usage (claw_id, date, input_tokens, output_tokens)
|
||||
VALUES (${claw.id}, ${targetDate}, ${inputTokens}, ${outputTokens})
|
||||
ON DUPLICATE KEY UPDATE
|
||||
input_tokens = ${inputTokens},
|
||||
output_tokens = ${outputTokens},
|
||||
updated_at = NOW()
|
||||
`);
|
||||
|
||||
// Invalidate leaderboard cache
|
||||
await invalidateTokenLeaderboardCache();
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
date: targetDate,
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
totalTokens: inputTokens + outputTokens,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Token report error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user