Skip to content

Commit 21aff30

Browse files
authored
Merge pull request #35 from sxjeru/2144
feat: add credential name mapping and name filter
2 parents 0ebf80e + af9e441 commit 21aff30

17 files changed

Lines changed: 1278 additions & 220 deletions

app/api/explore/route.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ const EXPLORE_CACHE_TTL_MS = 30_000;
1313
const EXPLORE_CACHE_MAX_ENTRIES = 100;
1414
const exploreCache = new Map<string, CachedExplore>();
1515

16-
function makeCacheKey(input: { days?: number; maxPoints?: number; start?: string | null; end?: string | null }) {
16+
function makeCacheKey(input: { days?: number; maxPoints?: number; start?: string | null; end?: string | null; route?: string | null; name?: string | null }) {
1717
return JSON.stringify({
1818
days: input.days ?? null,
1919
maxPoints: input.maxPoints ?? null,
2020
start: input.start ?? null,
21-
end: input.end ?? null
21+
end: input.end ?? null,
22+
route: input.route ?? null,
23+
name: input.name ?? null
2224
});
2325
}
2426

@@ -53,17 +55,19 @@ export async function GET(request: Request) {
5355
const maxPointsParam = searchParams.get("maxPoints");
5456
const start = searchParams.get("start");
5557
const end = searchParams.get("end");
58+
const route = searchParams.get("route");
59+
const name = searchParams.get("name");
5660

5761
const days = daysParam ? Number.parseInt(daysParam, 10) : undefined;
5862
const maxPoints = maxPointsParam ? Number.parseInt(maxPointsParam, 10) : undefined;
5963

60-
const cacheKey = makeCacheKey({ days, maxPoints, start, end });
64+
const cacheKey = makeCacheKey({ days, maxPoints, start, end, route, name });
6165
const cached = getCached(cacheKey);
6266
if (cached) {
6367
return NextResponse.json(cached, { status: 200 });
6468
}
6569

66-
const payload = await getExplorePoints(days, { maxPoints, start, end });
70+
const payload = await getExplorePoints(days, { maxPoints, start, end, route, name });
6771
setCached(cacheKey, payload);
6872
return NextResponse.json(payload, { status: 200 });
6973
} catch (error) {

app/api/overview/route.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ const OVERVIEW_CACHE_TTL_MS = 30_000;
2020
const OVERVIEW_CACHE_MAX_ENTRIES = 100;
2121
const overviewCache = new Map<string, CachedOverview>();
2222

23-
function makeCacheKey(input: { days?: number; model?: string | null; route?: string | null; page?: number; pageSize?: number; start?: string | null; end?: string | null }) {
23+
function makeCacheKey(input: { days?: number; model?: string | null; route?: string | null; source?: string | null; name?: string | null; page?: number; pageSize?: number; start?: string | null; end?: string | null }) {
2424
return JSON.stringify({
2525
days: input.days ?? null,
2626
model: input.model ?? null,
2727
route: input.route ?? null,
28+
source: input.source ?? null,
29+
name: input.name ?? null,
2830
page: input.page ?? null,
2931
pageSize: input.pageSize ?? null,
3032
start: input.start ?? null,
@@ -63,6 +65,8 @@ export async function GET(request: Request) {
6365
const days = daysParam ? Number.parseInt(daysParam, 10) : undefined;
6466
const model = searchParams.get("model");
6567
const route = searchParams.get("route");
68+
const source = searchParams.get("source");
69+
const name = searchParams.get("name");
6670
const pageParam = searchParams.get("page");
6771
const pageSizeParam = searchParams.get("pageSize");
6872
const start = searchParams.get("start");
@@ -72,7 +76,7 @@ export async function GET(request: Request) {
7276
const pageSize = pageSizeParam ? Number.parseInt(pageSizeParam, 10) : undefined;
7377
const skipCacheParam = searchParams.get("skipCache");
7478
const skipCache = skipCacheParam === "1" || skipCacheParam === "true";
75-
const cacheKey = makeCacheKey({ days, model, route, page, pageSize, start, end });
79+
const cacheKey = makeCacheKey({ days, model, route, source, name, page, pageSize, start, end });
7680
if (!skipCache) {
7781
const cached = getCached(cacheKey);
7882
if (cached) {
@@ -83,6 +87,8 @@ export async function GET(request: Request) {
8387
const { overview, empty, days: appliedDays, meta, filters, timezone } = await getOverview(days, {
8488
model: model || undefined,
8589
route: route || undefined,
90+
source: source || undefined,
91+
name: name || undefined,
8692
page,
8793
pageSize,
8894
start,

app/api/records/route.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export async function GET(request: Request) {
1818
| "occurredAt"
1919
| "model"
2020
| "route"
21+
| "source"
2122
| "totalTokens"
2223
| "inputTokens"
2324
| "outputTokens"
@@ -30,6 +31,7 @@ export async function GET(request: Request) {
3031
const cursor = searchParams.get("cursor");
3132
const model = searchParams.get("model");
3233
const route = searchParams.get("route");
34+
const source = searchParams.get("source");
3335
const start = searchParams.get("start");
3436
const end = searchParams.get("end");
3537
const includeFilters = searchParams.get("includeFilters") === "1";
@@ -43,6 +45,7 @@ export async function GET(request: Request) {
4345
cursor,
4446
model: model || undefined,
4547
route: route || undefined,
48+
source: source || undefined,
4649
start,
4750
end,
4851
includeFilters

app/api/sync/route.ts

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { cookies } from "next/headers";
33
import { eq, sql } from "drizzle-orm";
44
import { config, assertEnv } from "@/lib/config";
55
import { db } from "@/lib/db/client";
6-
import { usageRecords } from "@/lib/db/schema";
6+
import { authFileMappings, usageRecords } from "@/lib/db/schema";
7+
import { toAuthFileMappings } from "@/lib/auth-files";
78
import { parseUsagePayload, toUsageRecords } from "@/lib/usage";
89

910
export const runtime = "nodejs";
@@ -47,6 +48,44 @@ async function isAuthorized(request: Request) {
4748
return false;
4849
}
4950

51+
async function syncAuthFileMappings(pulledAt: Date) {
52+
const authFilesUrl = `${config.cliproxy.baseUrl.replace(/\/$/, "")}/auth-files`;
53+
54+
const response = await fetch(authFilesUrl, {
55+
headers: {
56+
Authorization: `Bearer ${config.cliproxy.apiKey}`,
57+
"Content-Type": "application/json"
58+
},
59+
cache: "no-store"
60+
});
61+
62+
if (!response.ok) {
63+
throw new Error(`Failed to fetch auth-files: ${response.status} ${response.statusText}`);
64+
}
65+
66+
const json = await response.json();
67+
const rows = toAuthFileMappings(json, pulledAt);
68+
if (rows.length === 0) return 0;
69+
70+
await db
71+
.insert(authFileMappings)
72+
.values(rows)
73+
.onConflictDoUpdate({
74+
target: authFileMappings.authId,
75+
set: {
76+
name: sql`coalesce(nullif(excluded.name, ''), ${authFileMappings.name})`,
77+
label: sql`coalesce(nullif(excluded.label, ''), ${authFileMappings.label})`,
78+
provider: sql`coalesce(nullif(excluded.provider, ''), ${authFileMappings.provider})`,
79+
source: sql`coalesce(nullif(excluded.source, ''), ${authFileMappings.source})`,
80+
email: sql`coalesce(nullif(excluded.email, ''), ${authFileMappings.email})`,
81+
updatedAt: sql`coalesce(excluded.updated_at, ${authFileMappings.updatedAt})`,
82+
syncedAt: pulledAt
83+
}
84+
});
85+
86+
return rows.length;
87+
}
88+
5089
async function performSync(request: Request) {
5190
if (!config.password && !config.cronSecret && !PASSWORD) return missingPassword();
5291
if (!(await isAuthorized(request))) return unauthorized();
@@ -89,16 +128,31 @@ async function performSync(request: Request) {
89128

90129
const rows = toUsageRecords(payload, pulledAt);
91130

131+
let authFilesSynced = 0;
132+
let authFilesWarning: string | undefined;
133+
try {
134+
authFilesSynced = await syncAuthFileMappings(pulledAt);
135+
} catch (error) {
136+
authFilesWarning = "auth-files sync failed";
137+
console.warn("/api/sync auth-files sync failed:", error);
138+
}
139+
92140
if (rows.length === 0) {
93-
return NextResponse.json({ status: "ok", inserted: 0, message: "No usage data" });
141+
return NextResponse.json({
142+
status: "ok",
143+
inserted: 0,
144+
message: "No usage data",
145+
authFilesSynced,
146+
...(authFilesWarning ? { authFilesWarning } : {})
147+
});
94148
}
95149

96150
let insertedRows: Array<{ id: number }>;
97151
try {
98152
insertedRows = await db
99153
.insert(usageRecords)
100154
.values(rows)
101-
.onConflictDoNothing({ target: [usageRecords.occurredAt, usageRecords.route, usageRecords.model] })
155+
.onConflictDoNothing({ target: [usageRecords.occurredAt, usageRecords.route, usageRecords.model, usageRecords.source] })
102156
.returning({ id: usageRecords.id });
103157
} catch (dbError) {
104158
console.error("/api/sync database insert failed:", dbError);
@@ -119,7 +173,13 @@ async function performSync(request: Request) {
119173
inserted = Number(fallback?.[0]?.count ?? 0);
120174
}
121175

122-
return NextResponse.json({ status: "ok", inserted, attempted: rows.length });
176+
return NextResponse.json({
177+
status: "ok",
178+
inserted,
179+
attempted: rows.length,
180+
authFilesSynced,
181+
...(authFilesWarning ? { authFilesWarning } : {})
182+
});
123183
}
124184

125185
export async function POST(request: Request) {

0 commit comments

Comments
 (0)