Skip to content

Commit c01435d

Browse files
committed
feat: add provider analytics to AIGatewayAnalytics and log model provider in AIGatewayLogs
1 parent 0df9ee6 commit c01435d

7 files changed

Lines changed: 172 additions & 2 deletions

File tree

src/client/components/aiGateway/AIGatewayAnalytics.tsx

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export const AIGatewayAnalytics: React.FC<AIGatewayAnalyticsProps> = React.memo(
7070
<Tabs defaultValue="performance" className="w-full">
7171
<TabsList>
7272
<TabsTrigger value="performance">{t('Performance')}</TabsTrigger>
73+
<TabsTrigger value="providers">{t('Providers')}</TabsTrigger>
7374
<TabsTrigger value="models">{t('Models')}</TabsTrigger>
7475
<TabsTrigger value="users">{t('Users')}</TabsTrigger>
7576
<TabsTrigger value="errors">{t('Errors')}</TabsTrigger>
@@ -217,6 +218,153 @@ export const AIGatewayAnalytics: React.FC<AIGatewayAnalyticsProps> = React.memo(
217218
</div>
218219
</TabsContent>
219220

221+
{/* Provider Analytics */}
222+
<TabsContent value="providers" className="space-y-4">
223+
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
224+
<Card>
225+
<CardHeader>
226+
<CardTitle>{t('Request Count by Provider')}</CardTitle>
227+
<p className="text-muted-foreground text-sm">
228+
{t(
229+
'Total request count distribution across different upstream providers (e.g. openai, openrouter, anthropic)'
230+
)}
231+
</p>
232+
</CardHeader>
233+
<CardContent>
234+
<InsightQueryChart
235+
className="h-[300px] w-full"
236+
workspaceId={workspaceId}
237+
insightId={gatewayId}
238+
insightType="aigateway"
239+
metrics={[{ name: '$all_event', math: 'events' }]}
240+
filters={[]}
241+
groups={[{ value: 'modelProvider', type: 'string' }]}
242+
time={timeConfig}
243+
chartType="pie"
244+
valueProcessor={defaultValueProcessor.alwaysPositive}
245+
/>
246+
</CardContent>
247+
</Card>
248+
249+
<Card>
250+
<CardHeader>
251+
<CardTitle>{t('Cost by Provider')}</CardTitle>
252+
<p className="text-muted-foreground text-sm">
253+
{t(
254+
'Total cost distribution across different upstream providers'
255+
)}
256+
</p>
257+
</CardHeader>
258+
<CardContent>
259+
<InsightQueryChart
260+
className="h-[300px] w-full"
261+
workspaceId={workspaceId}
262+
insightId={gatewayId}
263+
insightType="aigateway"
264+
metrics={[{ name: 'price', math: 'events' }]}
265+
filters={[]}
266+
groups={[{ value: 'modelProvider', type: 'string' }]}
267+
time={timeConfig}
268+
chartType="bar"
269+
valueProcessor={defaultValueProcessor.alwaysPositive}
270+
/>
271+
</CardContent>
272+
</Card>
273+
</div>
274+
275+
<Card>
276+
<CardHeader>
277+
<CardTitle>{t('Provider Request Trend')}</CardTitle>
278+
<p className="text-muted-foreground text-sm">
279+
{t(
280+
'Request volume over time grouped by upstream providers'
281+
)}
282+
</p>
283+
</CardHeader>
284+
<CardContent>
285+
<InsightQueryChart
286+
className="h-[300px] w-full"
287+
workspaceId={workspaceId}
288+
insightId={gatewayId}
289+
insightType="aigateway"
290+
metrics={[{ name: '$all_event', math: 'events' }]}
291+
filters={[]}
292+
groups={[{ value: 'modelProvider', type: 'string' }]}
293+
time={timeConfig}
294+
chartType="line"
295+
valueProcessor={defaultValueProcessor.alwaysPositive}
296+
/>
297+
</CardContent>
298+
</Card>
299+
300+
<Card>
301+
<CardHeader>
302+
<CardTitle>{t('Token Usage by Provider')}</CardTitle>
303+
<p className="text-muted-foreground text-sm">
304+
{t(
305+
'Total token consumption (input + output + cache) distribution across different upstream providers'
306+
)}
307+
</p>
308+
</CardHeader>
309+
<CardContent>
310+
<InsightQueryChart
311+
className="h-[400px] w-full"
312+
workspaceId={workspaceId}
313+
insightId={gatewayId}
314+
insightType="aigateway"
315+
metrics={[
316+
{ name: 'inputToken', math: 'events', alias: 'inputToken' },
317+
{
318+
name: 'outputToken',
319+
math: 'events',
320+
alias: 'outputToken',
321+
},
322+
{
323+
name: 'cacheReadInputToken',
324+
math: 'events',
325+
alias: 'cacheReadInputToken',
326+
},
327+
{
328+
name: 'cacheWriteInputToken',
329+
math: 'events',
330+
alias: 'cacheWriteInputToken',
331+
},
332+
]}
333+
filters={[]}
334+
groups={[{ value: 'modelProvider', type: 'string' }]}
335+
time={timeConfig}
336+
chartType="bar"
337+
valueProcessor={defaultValueProcessor.alwaysPositive}
338+
/>
339+
</CardContent>
340+
</Card>
341+
342+
<Card>
343+
<CardHeader>
344+
<CardTitle>{t('Average Response Time by Provider')}</CardTitle>
345+
<p className="text-muted-foreground text-sm">
346+
{t(
347+
'Average response time (duration) comparison across different upstream providers'
348+
)}
349+
</p>
350+
</CardHeader>
351+
<CardContent>
352+
<InsightQueryChart
353+
className="h-[300px] w-full"
354+
workspaceId={workspaceId}
355+
insightId={gatewayId}
356+
insightType="aigateway"
357+
metrics={[{ name: 'duration', math: 'avg' }]}
358+
filters={[]}
359+
groups={[{ value: 'modelProvider', type: 'string' }]}
360+
time={timeConfig}
361+
chartType="bar"
362+
valueProcessor={defaultValueProcessor.alwaysPositive}
363+
/>
364+
</CardContent>
365+
</Card>
366+
</TabsContent>
367+
220368
{/* Model Analytics */}
221369
<TabsContent value="models" className="space-y-4">
222370
<Card>

src/client/components/aiGateway/useAIGatewayLogColumns.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ export function useAIGatewayLogColumns(onRowSelect?: (index: number) => void) {
4040
);
4141
},
4242
}),
43+
columnHelper.accessor('modelProvider', {
44+
header: t('Provider'),
45+
size: 100,
46+
cell: (props) => {
47+
const val = props.getValue();
48+
if (!val) {
49+
return (
50+
<span className="text-muted-foreground opacity-50">-</span>
51+
);
52+
}
53+
return val;
54+
},
55+
}),
4356
columnHelper.accessor('modelName', {
4457
header: t('Model'),
4558
size: 120,

src/server/model/aiGateway.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,16 @@ export function buildOpenAIHandler(
8585
: payload.model;
8686

8787
const start = Date.now();
88+
const modelProvider =
89+
options.modelProvider ?? (options.isCustomRoute ? 'custom' : 'openai');
8890

8991
const logP = new Promise<AIGatewayLogs>(async (resolve) => {
9092
const _log = await prisma.aIGatewayLogs.create({
9193
data: {
9294
workspaceId,
9395
gatewayId,
9496
modelName,
97+
modelProvider,
9598
stream,
9699
inputToken: 0,
97100
outputToken: 0,
@@ -115,7 +118,6 @@ export function buildOpenAIHandler(
115118
baseURL: baseUrl,
116119
defaultHeaders: options.header?.(req),
117120
});
118-
const modelProvider = options.modelProvider ?? 'openai';
119121
const modelPriceName = options.modelPriceName
120122
? options.modelPriceName(modelName)
121123
: modelName;
@@ -416,14 +418,16 @@ export function buildAnthropicHandler(
416418
: payload.model;
417419

418420
const start = Date.now();
419-
const modelProvider = options.modelProvider ?? 'anthropic';
421+
const modelProvider =
422+
options.modelProvider ?? (options.isCustomRoute ? 'custom' : 'anthropic');
420423

421424
const logP = new Promise<AIGatewayLogs>(async (resolve) => {
422425
const _log = await prisma.aIGatewayLogs.create({
423426
data: {
424427
workspaceId,
425428
gatewayId,
426429
modelName,
430+
modelProvider,
427431
stream,
428432
inputToken: 0,
429433
outputToken: 0,

src/server/model/insights/aiGateway.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export class AIGatewayInsightsSqlBuilder extends InsightsSqlBuilder {
121121
[
122122
'gatewayId',
123123
'modelName',
124+
'modelProvider',
124125
'status',
125126
'stream',
126127
'inputToken',
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable: Add modelProvider column to AIGatewayLogs
2+
ALTER TABLE "AIGatewayLogs" ADD COLUMN "modelProvider" TEXT NOT NULL DEFAULT '';

src/server/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,7 @@ model AIGatewayLogs {
981981
cacheWriteInputToken Int @default(0) @db.Integer
982982
stream Boolean @default(false) @db.Boolean // stream response or not
983983
modelName String
984+
modelProvider String @default("") // upstream provider, e.g. openai, anthropic, openrouter, deepseek, custom
984985
status AIGatewayLogsStatus
985986
duration Int // All response usage time, unit: ms
986987
ttft Int @default(-1) // Time To First Token, unit: ms

src/server/prisma/zod/aigatewaylogs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const AIGatewayLogsModelSchema = z.object({
3434
cacheWriteInputToken: z.number().int(),
3535
stream: z.boolean(),
3636
modelName: z.string(),
37+
modelProvider: z.string(),
3738
status: z.nativeEnum(AIGatewayLogsStatus),
3839
duration: z.number().int(),
3940
ttft: z.number().int(),

0 commit comments

Comments
 (0)