Commit 9f8b75fd by Archer Committed by GitHub

Ai proxy monitor (#5009)

* Time granularity and Table of a single model (#4990)

* Aiproxy ModelBoard

* Add components LineChartComponent and Make some revisions

* Time granularity and Table of a single model

* Modify the logic and sort the tables in ascending or descending order

* Use theme and present in seconds

* Add the channel name section (#5005)

* Add components LineChartComponent and Make some revisions

* Time granularity and Table of a single model

* Modify the logic and sort the tables in ascending or descending order

* Add the channel name section

* The channel_name is transmitted from the outer layer

* Restore the channel

* perf: dashboard code

* perf: ai proxy monitor

* code

---------

Co-authored-by: Zhuangzai fa <143257420+ctrlz526@users.noreply.github.com>
parent abbfc436
......@@ -9,8 +9,8 @@ weight: 788
## 🚀 新增内容
1. AI proxy 服务,支持以图表形式展示模型调用情况。
1. 商业版支持知识库分块时,LLM 进行自动分段识别。
1. AI proxy 监控完善,支持以图表/表格形式查看模型调用和性能情况。
2. 商业版支持知识库分块时,LLM 进行自动分段识别。
## ⚙️ 优化
......
......@@ -3,6 +3,8 @@
"aipoint_usage": "AI points",
"all": "All",
"api_key": "API key",
"avg_response_time": "Average call time (seconds)",
"avg_ttfb": "Average first word duration (seconds)",
"azure": "Azure",
"base_url": "Base url",
"channel_name": "Channel",
......@@ -14,14 +16,28 @@
"channel_status_enabled": "Enable",
"channel_status_unknown": "unknown",
"channel_type": "Manufacturer",
"chart_mode_cumulative": "Cumulative",
"chart_mode_incremental": "Incremental",
"clear_model": "Clear the model",
"confirm_delete_channel": "Confirm the deletion of the [{{name}}] channel?",
"copy_model_id_success": "Copyed model id",
"create_channel": "Added channels",
"dashboard_call_trend": "Model Call Trend",
"dashboard_channel": "Channel",
"dashboard_cost_trend": "Cost Consumption",
"dashboard_error_calls": "Error Calls",
"dashboard_input_tokens": "Input Tokens",
"dashboard_model": "Model",
"dashboard_no_data": "No data available",
"dashboard_output_tokens": "Output Tokens",
"dashboard_points": "points",
"dashboard_success_calls": "Success Calls",
"dashboard_token_trend": "Token Usage Trend",
"dashboard_token_usage": "Tokens",
"dashboard_total_calls": "Total Calls:",
"dashboard_total_cost": "Total Cost",
"dashboard_total_cost_label": "Total Cost:",
"dashboard_total_tokens": "Total Tokens",
"default_url": "Default address",
"detail": "Detail",
"duration": "Duration",
......@@ -38,7 +54,9 @@
"mapping": "Model Mapping",
"mapping_tip": "A valid Json is required. \nThe model can be mapped when sending a request to the actual address. \nFor example:\n{\n \n \"gpt-4o\": \"gpt-4o-test\"\n\n}\n\nWhen FastGPT requests the gpt-4o model, the gpt-4o-test model is sent to the actual address, instead of gpt-4o.",
"maxToken_tip": "Model max_tokens parameter",
"max_rpm": "Max RPM (Requests Per Minute)",
"max_temperature_tip": "If the model temperature parameter is not filled in, it means that the model does not support the temperature parameter.",
"max_tpm": "Max TPM (Tokens Per Minute)",
"model": "Model",
"model_error_rate": "Error rate",
"model_error_request_times": "Number of failures",
......@@ -60,7 +78,15 @@
"selected_model_empty": "Choose at least one model",
"start_test": "Batch test {{num}} models",
"test_failed": "There are {{num}} models that report errors",
"timespan_day": "Day",
"timespan_hour": "Hour",
"timespan_label": "Time Granularity",
"timespan_minute": "Minute",
"total_call_volume": "Request amount",
"view_chart": "Chart",
"view_table": "Table",
"vlm_model": "Vlm",
"vlm_model_tip": "Used to generate additional indexing of images in a document in the knowledge base",
"volunme_of_failed_calls": "Error amount",
"waiting_test": "Waiting for testing"
}
{
"Hunyuan": "腾讯混元",
"aipoint_usage": "积分消耗",
"all": "全部",
"api_key": "API 密钥",
"avg_response_time": "平均调用时长 (秒)",
"avg_ttfb": "平均首字时长 (秒)",
"azure": "微软 Azure",
"base_url": "代理地址",
"channel_name": "渠道名",
......@@ -13,15 +16,28 @@
"channel_status_enabled": "启用",
"channel_status_unknown": "未知",
"channel_type": "厂商",
"chart_mode_cumulative": "累积",
"chart_mode_incremental": "分时",
"clear_model": "清空模型",
"confirm_delete_channel": "确认删除 【{{name}}】渠道?",
"copy_model_id_success": "已复制模型id",
"create_channel": "新增渠道",
"aipoint_usage": "积分消耗",
"dashboard_call_trend": "模型调用次数趋势",
"dashboard_channel": "渠道",
"dashboard_cost_trend": "积分消耗",
"dashboard_error_calls": "错误次数",
"dashboard_input_tokens": "输入Tokens",
"dashboard_model": "模型",
"dashboard_no_data": "暂无数据",
"dashboard_output_tokens": "输出Tokens",
"dashboard_points": "积分",
"dashboard_success_calls": "成功次数",
"dashboard_token_trend": "Tokens使用趋势",
"dashboard_token_usage": "Tokens 消耗",
"dashboard_total_calls": "总调用次数:",
"dashboard_total_cost": "总成本",
"dashboard_total_cost_label": "总成本:",
"dashboard_total_tokens": "总Tokens",
"default_url": "默认地址",
"detail": "详情",
"duration": "耗时",
......@@ -38,7 +54,9 @@
"mapping": "模型映射",
"mapping_tip": "需填写一个有效 Json。可在向实际地址发送请求时,对模型进行映射。例如:\n{\n \"gpt-4o\": \"gpt-4o-test\"\n}\n当 FastGPT 请求 gpt-4o 模型时,会向实际地址发送 gpt-4o-test 的模型,而不是 gpt-4o。",
"maxToken_tip": "模型 max_tokens 参数",
"max_rpm": "最大RPM (每分钟请求数)",
"max_temperature_tip": "模型 temperature 参数,不填则代表模型不支持 temperature 参数。",
"max_tpm": "最大TPM (每分钟Token数)",
"model": "模型",
"model_error_rate": "失败率",
"model_error_request_times": "失败次数",
......@@ -60,7 +78,15 @@
"selected_model_empty": "至少选择一个模型",
"start_test": "批量测试{{num}}个模型",
"test_failed": "有{{num}}个模型报错",
"timespan_day": "天",
"timespan_hour": "小时",
"timespan_label": "时间颗粒度",
"timespan_minute": "分钟",
"total_call_volume": "调用总量",
"view_chart": "图表",
"view_table": "表格",
"vlm_model": "图片理解模型",
"vlm_model_tip": "用于知识库中对文档中的图片进行额外的索引生成",
"volunme_of_failed_calls": "调用失败量",
"waiting_test": "等待测试"
}
......@@ -3,6 +3,8 @@
"aipoint_usage": "積分消耗",
"all": "全部",
"api_key": "API 金鑰",
"avg_response_time": "平均調用時長 (秒)",
"avg_ttfb": "平均首字時長 (秒)",
"azure": "Azure",
"base_url": "代理地址",
"channel_name": "管道名稱",
......@@ -14,11 +16,28 @@
"channel_status_enabled": "啟用",
"channel_status_unknown": "未知",
"channel_type": "廠商",
"chart_mode_cumulative": "累積",
"chart_mode_incremental": "分時",
"clear_model": "清空模型",
"confirm_delete_channel": "確認刪除【{{name}}】管道?",
"copy_model_id_success": "已復制模型 id",
"create_channel": "新增管道",
"dashboard_call_trend": "模型呼叫次數趨勢",
"dashboard_channel": "管道",
"dashboard_cost_trend": "積分消耗",
"dashboard_error_calls": "錯誤次數",
"dashboard_input_tokens": "輸入Tokens",
"dashboard_model": "模型",
"dashboard_no_data": "暫無資料",
"dashboard_output_tokens": "輸出Tokens",
"dashboard_points": "積分",
"dashboard_success_calls": "成功次數",
"dashboard_token_trend": "Tokens使用趨勢",
"dashboard_token_usage": "Tokens 消耗",
"dashboard_total_calls": "總呼叫次數:",
"dashboard_total_cost": "總成本",
"dashboard_total_cost_label": "總成本:",
"dashboard_total_tokens": "總Tokens",
"default_url": "預設地址",
"detail": "詳細資訊",
"duration": "耗時",
......@@ -35,7 +54,9 @@
"mapping": "模型對映",
"mapping_tip": "需填寫一個有效 Json。\n可在向實際地址傳送請求時,對模型進行對映。\n例如:\n{\n \n \"gpt-4o\": \"gpt-4o-test\"\n\n}\n\n當 FastGPT 請求 gpt-4o 模型時,會向實際地址傳送 gpt-4o-test 的模型,而不是 gpt-4o。",
"maxToken_tip": "模型 max_tokens 參數",
"max_rpm": "最大RPM (每分鐘請求數)",
"max_temperature_tip": "模型 temperature 參數,不填則代表模型不支援 temperature 參數。",
"max_tpm": "最大TPM (每分鐘Token數)",
"model": "模型",
"model_error_rate": "失敗率",
"model_error_request_times": "失敗次數",
......@@ -57,7 +78,15 @@
"selected_model_empty": "至少選擇一個模型",
"start_test": "批次測試{{num}}個模型",
"test_failed": "有{{num}}個模型報錯",
"timespan_day": "天",
"timespan_hour": "小時",
"timespan_label": "時間顆粒度",
"timespan_minute": "分鐘",
"total_call_volume": "調用總量",
"view_chart": "圖表",
"view_table": "表格",
"vlm_model": "圖片理解模型",
"vlm_model_tip": "用於知識庫中對文件中的圖片進行額外的索引生成",
"volunme_of_failed_calls": "調用失敗量",
"waiting_test": "等待測試"
}
......@@ -54,11 +54,16 @@ export type ChannelLogListItemType = {
};
export type DashboardDataItemType = {
channel_id?: number;
model: string;
request_count: number;
used_amount: number;
exception_count: number;
request_count?: number;
used_amount?: number;
exception_count?: number;
total_time_milliseconds?: number;
total_ttfb_milliseconds?: number;
input_tokens?: number;
output_tokens?: number;
total_tokens?: number;
max_rpm?: number;
max_tpm?: number;
};
......@@ -12,10 +12,9 @@ import {
} from 'recharts';
import { type NameType, type ValueType } from 'recharts/types/component/DefaultTooltipContent';
import { formatNumber } from '@fastgpt/global/common/math/tools';
type XAxisConfig = {
dataKey: string;
};
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import { useTranslation } from 'next-i18next';
import { cloneDeep } from 'lodash';
type LineConfig = {
dataKey: string;
......@@ -29,15 +28,16 @@ type TooltipItem = {
dataKey: string;
color: string;
formatter?: (value: number) => string;
customValue?: (data: any) => number;
customValue?: (data: Record<string, any>) => number;
};
type LineChartComponentProps = {
data: Record<string, any>[];
title: string;
HeaderRightChildren?: React.ReactNode;
HeaderLeftChildren?: React.ReactNode;
lines: LineConfig[];
tooltipItems?: TooltipItem[];
enableCumulative?: boolean;
};
const CustomTooltip = ({
......@@ -47,48 +47,51 @@ const CustomTooltip = ({
}: TooltipProps<ValueType, NameType> & { tooltipItems?: TooltipItem[] }) => {
const data = payload?.[0]?.payload;
if (active && data && tooltipItems) {
return (
<Box bg={'white'} p={3} borderRadius={'md'} border={'base'} boxShadow={'sm'}>
<Box fontSize={'sm'} color={'myGray.900'} mb={2}>
{data.x}
</Box>
{tooltipItems.map((item, index) => {
const value = (() => {
if (item.customValue) {
return item.customValue(data);
} else {
return data[item.dataKey];
}
})();
const displayValue = (() => {
const val = item.formatter ? item.formatter(value) : formatNumber(value);
return val.toLocaleString();
})();
return (
<HStack key={index} fontSize={'sm'} _notLast={{ mb: 1 }}>
<Box w={2} h={2} borderRadius={'full'} bg={item.color} />
<Box>{item.label}</Box>
<Box>{displayValue}</Box>
</HStack>
);
})}
</Box>
);
if (!active || !data || !tooltipItems) {
return null;
}
return null;
return (
<Box bg="white" p={3} borderRadius="md" border="base" boxShadow="sm">
<Box fontSize="sm" color="myGray.900" mb={2}>
{data.xLabel || data.x}
</Box>
{tooltipItems.map((item, index) => {
const value = item.customValue ? item.customValue(data) : data[item.dataKey];
const displayValue = item.formatter ? item.formatter(value) : formatNumber(value);
return (
<HStack key={index} fontSize="sm" _notLast={{ mb: 1 }}>
<Box w={2} h={2} borderRadius="full" bg={item.color} />
<Box>{item.label}</Box>
<Box>{displayValue.toLocaleString()}</Box>
</HStack>
);
})}
</Box>
);
};
const LineChartComponent = ({
data,
title,
HeaderRightChildren,
HeaderLeftChildren,
lines,
tooltipItems
tooltipItems,
enableCumulative = true
}: LineChartComponentProps) => {
const theme = useTheme();
const { t } = useTranslation();
const [displayMode, setDisplayMode] = useState<'incremental' | 'cumulative'>('incremental');
// Tab list constant
const tabList = useMemo(
() => [
{ label: t('account_model:chart_mode_incremental'), value: 'incremental' as const },
{ label: t('account_model:chart_mode_cumulative'), value: 'cumulative' as const }
],
[t]
);
// Y-axis number formatter function
const formatYAxisNumber = useCallback((value: number): string => {
......@@ -100,11 +103,35 @@ const LineChartComponent = ({
return value.toString();
}, []);
// Process data based on display mode
const processedData = useMemo(() => {
if (displayMode === 'incremental' || !enableCumulative) {
return data;
}
// Cumulative mode: accumulate values for each line's dataKey
const cloneData = cloneDeep(data);
const dataKeys = lines.map((item) => item.dataKey);
return cloneData.map((item, index) => {
if (index === 0) return item;
dataKeys.forEach((key) => {
if (typeof item[key] === 'number') {
item[key] += cloneData[index - 1][key];
}
});
return item;
});
}, [data, displayMode, lines, enableCumulative]);
// Generate gradient definitions
const gradientDefs = useMemo(() => {
return (
const gradientDefs = useMemo(
() => (
<defs>
{lines.map((line, index) => (
{lines.map((line) => (
<linearGradient
key={`gradient-${line.color}`}
id={`gradient-${line.color}`}
......@@ -118,8 +145,9 @@ const LineChartComponent = ({
</linearGradient>
))}
</defs>
);
}, [lines]);
),
[lines]
);
return (
<>
......@@ -127,33 +155,44 @@ const LineChartComponent = ({
<Box fontSize={'sm'} color={'myGray.900'} fontWeight={'medium'}>
{title}
</Box>
{HeaderRightChildren && HeaderRightChildren}
<HStack spacing={2}>
{HeaderLeftChildren}
{enableCumulative && (
<FillRowTabs<'incremental' | 'cumulative'>
list={tabList}
py={0.5}
px={2}
value={displayMode}
onChange={setDisplayMode}
/>
)}
</HStack>
</HStack>
<ResponsiveContainer width="100%" height={'100%'}>
<AreaChart
data={data}
margin={{ top: 5, right: 30, left: 0, bottom: HeaderRightChildren ? 30 : 15 }}
data={processedData}
margin={{ top: 5, right: 30, left: 0, bottom: HeaderLeftChildren ? 20 : 15 }}
>
{gradientDefs}
<XAxis
dataKey={'x'}
dataKey="x"
tickMargin={10}
tick={{ fontSize: '12px', color: theme.colors.myGray['500'], fontWeight: '500' }}
interval={'preserveStartEnd'}
interval="preserveStartEnd"
/>
<YAxis
axisLine={false}
tickSize={0}
tickMargin={10}
tick={{ fontSize: '12px', color: theme.colors.myGray['500'], fontWeight: '500' }}
interval={'preserveStartEnd'}
interval="preserveStartEnd"
tickFormatter={formatYAxisNumber}
/>
<CartesianGrid strokeDasharray="3 3" horizontal={true} vertical={false} />
{tooltipItems && <Tooltip content={<CustomTooltip tooltipItems={tooltipItems} />} />}
{lines.map((line, index) => (
<Area
key={index}
key={line.dataKey}
type="monotone"
name={line.name}
dataKey={line.dataKey}
......
......@@ -191,22 +191,17 @@ export const getLogDetail = (id: number) =>
export const getDashboardV2 = (params: {
channel?: number;
model?: string;
start_timestamp?: number;
end_timestamp?: number;
timezone?: string;
timespan?: 'day' | 'hour';
timezone: string;
timespan: 'day' | 'hour' | 'minute';
}) =>
GET<
{
timestamp: number;
models: DashboardDataItemType[];
summary: DashboardDataItemType[];
}[]
>('/dashboardv2/', {
channel: params.channel,
start_timestamp: params.start_timestamp,
end_timestamp: params.end_timestamp,
timezone: params.timezone || 'Local',
timespan: params.timespan || 'day'
});
>('/dashboardv2/', params);
export { responseSuccess, checkRes, responseError, instance, request };
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment