diff --git a/src/client/components/monitor/MonitorInfo.tsx b/src/client/components/monitor/MonitorInfo.tsx index ed16c37..5549fb5 100644 --- a/src/client/components/monitor/MonitorInfo.tsx +++ b/src/client/components/monitor/MonitorInfo.tsx @@ -124,7 +124,7 @@ export const MonitorInfo: React.FC = React.memo((props) => { -
+
Monitored for {dayjs().diff(dayjs(monitorInfo.createdAt), 'days')}{' '} days
diff --git a/src/client/components/monitor/provider/index.ts b/src/client/components/monitor/provider/index.ts index fb5389f..9bb8029 100644 --- a/src/client/components/monitor/provider/index.ts +++ b/src/client/components/monitor/provider/index.ts @@ -3,10 +3,12 @@ import { MonitorInfo } from '../../../../types'; import { pingProvider } from './ping'; import { httpProvider } from './http'; import { MonitorProvider } from './types'; +import { openaiProvider } from './openai'; export const monitorProviders: MonitorProvider[] = [ pingProvider, // ping httpProvider, // http + openaiProvider, // http ]; export function getMonitorProvider(type: string) { @@ -20,7 +22,7 @@ export function getMonitorProvider(type: string) { export function getMonitorLink(info: MonitorInfo): React.ReactNode { const provider = getMonitorProvider(info.type); - if (!provider) { + if (!provider || !provider.link) { return null; } diff --git a/src/client/components/monitor/provider/openai.tsx b/src/client/components/monitor/provider/openai.tsx new file mode 100644 index 0000000..d7424ba --- /dev/null +++ b/src/client/components/monitor/provider/openai.tsx @@ -0,0 +1,63 @@ +import { Form, Input } from 'antd'; +import React from 'react'; +import { MonitorOverviewComponent, MonitorProvider } from './types'; +import { useCurrentWorkspaceId } from '../../../store/user'; +import { trpc } from '../../../api/trpc'; +import dayjs from 'dayjs'; +import { MonitorStatsBlock } from '../MonitorStatsBlock'; +import { isEmpty } from 'lodash-es'; + +export const MonitorOpenai: React.FC = React.memo(() => { + return ( + <> + + + + + ); +}); +MonitorOpenai.displayName = 'MonitorOpenai'; + +export const MonitorOpenaiOverview: MonitorOverviewComponent = React.memo( + (props) => { + const workspaceId = useCurrentWorkspaceId(); + const { data } = trpc.monitor.getStatus.useQuery({ + workspaceId, + monitorId: props.monitorId, + statusName: 'credit', + }); + + if (!data || !data.payload || typeof data.payload !== 'object') { + return null; + } + + const payload = data.payload as Record; + + if (isEmpty(payload.certInfo)) { + return null; + } + + return ( + + ); + } +); +MonitorOpenaiOverview.displayName = 'MonitorOpenaiOverview'; + +export const openaiProvider: MonitorProvider = { + label: 'OpenAI', + name: 'openai', + form: MonitorOpenai, + overview: [MonitorOpenaiOverview], +}; diff --git a/src/client/components/monitor/provider/types.ts b/src/client/components/monitor/provider/types.ts index bf35eee..2cf9110 100644 --- a/src/client/components/monitor/provider/types.ts +++ b/src/client/components/monitor/provider/types.ts @@ -3,7 +3,7 @@ import { MonitorInfo } from '../../../../types'; export interface MonitorProvider { label: string; name: string; - link: (info: MonitorInfo) => React.ReactNode; + link?: (info: MonitorInfo) => React.ReactNode; form: React.ComponentType; overview?: MonitorOverviewComponent[]; } diff --git a/src/server/model/monitor/provider/_utils.ts b/src/server/model/monitor/provider/_utils.ts new file mode 100644 index 0000000..4291216 --- /dev/null +++ b/src/server/model/monitor/provider/_utils.ts @@ -0,0 +1,32 @@ +import { prisma } from '../../_client'; + +export async function saveMonitorStatus( + monitorId: string, + statusName: string, + payload: Record +) { + try { + const res = await prisma.monitorStatus.upsert({ + where: { + monitorId_statusName: { + monitorId, + statusName, + }, + }, + update: { + payload, + }, + create: { + monitorId, + statusName, + payload, + }, + }); + + return res; + } catch (err) { + console.error(err); + + return null; + } +} diff --git a/src/server/model/monitor/provider/http.ts b/src/server/model/monitor/provider/http.ts index 5b3d7ef..0c92c48 100644 --- a/src/server/model/monitor/provider/http.ts +++ b/src/server/model/monitor/provider/http.ts @@ -4,6 +4,7 @@ import { logger } from '../../../utils/logger'; import dayjs from 'dayjs'; import { prisma } from '../../_client'; import https from 'https'; +import { saveMonitorStatus } from './_utils'; export const http: MonitorProvider<{ url: string; @@ -81,27 +82,9 @@ export const http: MonitorProvider<{ try { const { valid, certInfo } = checkCertificate(res); - await prisma.monitorStatus.upsert({ - where: { - monitorId_statusName: { - monitorId: monitor.id, - statusName: 'tls', - }, - }, - update: { - payload: { - valid, - certInfo, - }, - }, - create: { - monitorId: monitor.id, - statusName: 'tls', - payload: { - valid, - certInfo, - }, - }, + await saveMonitorStatus(monitor.id, 'tls', { + valid, + certInfo, }); } catch (err) {} } diff --git a/src/server/model/monitor/provider/index.ts b/src/server/model/monitor/provider/index.ts index b6840c5..7339fea 100644 --- a/src/server/model/monitor/provider/index.ts +++ b/src/server/model/monitor/provider/index.ts @@ -1,8 +1,10 @@ import { http } from './http'; import { ping } from './ping'; +import { openai } from './openai'; import type { MonitorProvider } from './type'; export const monitorProviders: Record> = { ping, http, + openai, }; diff --git a/src/server/model/monitor/provider/openai.ts b/src/server/model/monitor/provider/openai.ts new file mode 100644 index 0000000..22f9e6d --- /dev/null +++ b/src/server/model/monitor/provider/openai.ts @@ -0,0 +1,68 @@ +import { z } from 'zod'; +import { MonitorProvider } from './type'; +import axios from 'axios'; +import { saveMonitorStatus } from './_utils'; + +const openaiCreditGrantsSchema = z.object({ + object: z.string(), + total_granted: z.number(), + total_used: z.number(), + total_available: z.number(), + total_paid_available: z.number(), + grants: z.object({ + object: z.string(), + data: z.array( + z.object({ + object: z.string(), + id: z.string(), + grant_amount: z.number(), + used_amount: z.number(), + effective_at: z.number(), + expires_at: z.number(), + }) + ), + }), +}); + +export const openai: MonitorProvider<{ + sessionKey: string; +}> = { + run: async (monitor) => { + if (typeof monitor.payload !== 'object') { + throw new Error('monitor.payload should be object'); + } + + const { sessionKey } = monitor.payload; + + const res = await getBillingCreditGrants(sessionKey); + + const balance = res.total_granted - res.total_used; + + await saveMonitorStatus(monitor.id, 'credit', { + totalGranted: res.total_granted, + totalUsed: res.total_used, + totalAvailable: res.total_available, + totalPaidAvailable: res.total_paid_available, + }); + + if (balance <= 0) { + return -1; + } + + return balance; + }, +}; + +async function getBillingCreditGrants(sessionKey: string) { + const { data } = await axios.get( + 'https://api.openai.com/dashboard/billing/credit_grants', + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${sessionKey}`, + }, + } + ); + + return openaiCreditGrantsSchema.parse(data); +}