mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-07 03:45:25 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f082d72bb | ||
|
|
0fd0e5d309 | ||
|
|
d2297d2723 | ||
|
|
62ae46b552 | ||
|
|
0b1354ed51 | ||
|
|
132c71390c | ||
|
|
bb3deb7b93 | ||
|
|
f92d96e298 | ||
|
|
c86762b656 | ||
|
|
3409d7a6b6 | ||
|
|
bfba4866a5 |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.github
|
||||
.git
|
||||
*.md
|
||||
.vscode
|
||||
.gitignore
|
||||
Makefile
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,4 +8,5 @@ build
|
||||
logs
|
||||
web/dist
|
||||
.env
|
||||
one-api
|
||||
one-api
|
||||
.DS_Store
|
||||
@@ -356,7 +356,7 @@ func GetCompletionRatio(name string) float64 {
|
||||
}
|
||||
return 2
|
||||
}
|
||||
if strings.HasPrefix(name, "o1-") {
|
||||
if strings.HasPrefix(name, "o1") {
|
||||
return 4
|
||||
}
|
||||
if name == "chatgpt-4o-latest" {
|
||||
|
||||
@@ -245,12 +245,12 @@ func removeAdditionalPropertiesWithDepth(schema interface{}, depth int) interfac
|
||||
if !ok || len(v) == 0 {
|
||||
return schema
|
||||
}
|
||||
|
||||
// 删除所有的title字段
|
||||
delete(v, "title")
|
||||
// 如果type不为object和array,则直接返回
|
||||
if typeVal, exists := v["type"]; !exists || (typeVal != "object" && typeVal != "array") {
|
||||
return schema
|
||||
}
|
||||
delete(v, "title")
|
||||
switch v["type"] {
|
||||
case "object":
|
||||
delete(v, "additionalProperties")
|
||||
|
||||
@@ -106,7 +106,7 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re
|
||||
if request == nil {
|
||||
return nil, errors.New("request is nil")
|
||||
}
|
||||
if info.ChannelType != common.ChannelTypeOpenAI {
|
||||
if info.ChannelType != common.ChannelTypeOpenAI && info.ChannelType != common.ChannelTypeAzure {
|
||||
request.StreamOptions = nil
|
||||
}
|
||||
if strings.HasPrefix(request.Model, "o1") {
|
||||
|
||||
@@ -109,7 +109,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo {
|
||||
}
|
||||
if info.ChannelType == common.ChannelTypeOpenAI || info.ChannelType == common.ChannelTypeAnthropic ||
|
||||
info.ChannelType == common.ChannelTypeAws || info.ChannelType == common.ChannelTypeGemini ||
|
||||
info.ChannelType == common.ChannelCloudflare {
|
||||
info.ChannelType == common.ChannelCloudflare || info.ChannelType == common.ChannelTypeAzure {
|
||||
info.SupportStreamOptions = true
|
||||
}
|
||||
return info
|
||||
|
||||
@@ -108,10 +108,17 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) {
|
||||
}
|
||||
}
|
||||
|
||||
promptTokens, err := getPromptTokens(textRequest, relayInfo)
|
||||
// count messages token error 计算promptTokens错误
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
|
||||
// 获取 promptTokens,如果上下文中已经存在,则直接使用
|
||||
var promptTokens int
|
||||
if value, exists := c.Get("prompt_tokens"); exists {
|
||||
promptTokens = value.(int)
|
||||
} else {
|
||||
promptTokens, err = getPromptTokens(textRequest, relayInfo)
|
||||
// count messages token error 计算promptTokens错误
|
||||
if err != nil {
|
||||
return service.OpenAIErrorWrapper(err, "count_token_messages_failed", http.StatusInternalServerError)
|
||||
}
|
||||
c.Set("prompt_tokens", promptTokens)
|
||||
}
|
||||
|
||||
if !getModelPriceSuccess {
|
||||
|
||||
@@ -28,10 +28,10 @@ func SetApiRouter(router *gin.Engine) {
|
||||
apiRouter.GET("/oauth/linuxdo", middleware.CriticalRateLimit(), controller.LinuxdoOAuth)
|
||||
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode)
|
||||
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
||||
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
||||
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
|
||||
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), controller.WeChatBind)
|
||||
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), controller.EmailBind)
|
||||
apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), controller.TelegramLogin)
|
||||
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.TelegramBind)
|
||||
apiRouter.GET("/oauth/telegram/bind", middleware.CriticalRateLimit(), controller.TelegramBind)
|
||||
|
||||
userRoute := apiRouter.Group("/user")
|
||||
{
|
||||
|
||||
@@ -19,42 +19,40 @@ import (
|
||||
// tokenEncoderMap won't grow after initialization
|
||||
var tokenEncoderMap = map[string]*tiktoken.Tiktoken{}
|
||||
var defaultTokenEncoder *tiktoken.Tiktoken
|
||||
var cl200kTokenEncoder *tiktoken.Tiktoken
|
||||
var o200kTokenEncoder *tiktoken.Tiktoken
|
||||
|
||||
func InitTokenEncoders() {
|
||||
common.SysLog("initializing token encoders")
|
||||
gpt35TokenEncoder, err := tiktoken.EncodingForModel("gpt-3.5-turbo")
|
||||
cl100TokenEncoder, err := tiktoken.GetEncoding(tiktoken.MODEL_CL100K_BASE)
|
||||
if err != nil {
|
||||
common.FatalLog(fmt.Sprintf("failed to get gpt-3.5-turbo token encoder: %s", err.Error()))
|
||||
}
|
||||
defaultTokenEncoder = gpt35TokenEncoder
|
||||
gpt4TokenEncoder, err := tiktoken.EncodingForModel("gpt-4")
|
||||
if err != nil {
|
||||
common.FatalLog(fmt.Sprintf("failed to get gpt-4 token encoder: %s", err.Error()))
|
||||
}
|
||||
cl200kTokenEncoder, err = tiktoken.EncodingForModel("gpt-4o")
|
||||
defaultTokenEncoder = cl100TokenEncoder
|
||||
o200kTokenEncoder, err = tiktoken.GetEncoding(tiktoken.MODEL_O200K_BASE)
|
||||
if err != nil {
|
||||
common.FatalLog(fmt.Sprintf("failed to get gpt-4o token encoder: %s", err.Error()))
|
||||
}
|
||||
for model, _ := range common.GetDefaultModelRatioMap() {
|
||||
if strings.HasPrefix(model, "gpt-3.5") {
|
||||
tokenEncoderMap[model] = gpt35TokenEncoder
|
||||
tokenEncoderMap[model] = cl100TokenEncoder
|
||||
} else if strings.HasPrefix(model, "gpt-4") {
|
||||
if strings.HasPrefix(model, "gpt-4o") {
|
||||
tokenEncoderMap[model] = cl200kTokenEncoder
|
||||
tokenEncoderMap[model] = o200kTokenEncoder
|
||||
} else {
|
||||
tokenEncoderMap[model] = gpt4TokenEncoder
|
||||
tokenEncoderMap[model] = defaultTokenEncoder
|
||||
}
|
||||
} else if strings.HasPrefix(model, "o1") {
|
||||
tokenEncoderMap[model] = o200kTokenEncoder
|
||||
} else {
|
||||
tokenEncoderMap[model] = nil
|
||||
tokenEncoderMap[model] = defaultTokenEncoder
|
||||
}
|
||||
}
|
||||
common.SysLog("token encoders initialized")
|
||||
}
|
||||
|
||||
func getModelDefaultTokenEncoder(model string) *tiktoken.Tiktoken {
|
||||
if strings.HasPrefix(model, "gpt-4o") || strings.HasPrefix(model, "chatgpt-4o") {
|
||||
return cl200kTokenEncoder
|
||||
if strings.HasPrefix(model, "gpt-4o") || strings.HasPrefix(model, "chatgpt-4o") || strings.HasPrefix(model, "o1") {
|
||||
return o200kTokenEncoder
|
||||
}
|
||||
return defaultTokenEncoder
|
||||
}
|
||||
@@ -92,11 +90,11 @@ func getImageToken(imageUrl *dto.MessageImageUrl, model string, stream bool) (in
|
||||
}
|
||||
// TODO: 非流模式下不计算图片token数量
|
||||
if !constant.GetMediaTokenNotStream && !stream {
|
||||
return 1000, nil
|
||||
return 256, nil
|
||||
}
|
||||
// 是否统计图片token
|
||||
if !constant.GetMediaToken {
|
||||
return 1000, nil
|
||||
return 256, nil
|
||||
}
|
||||
// 同步One API的图片计费逻辑
|
||||
if imageUrl.Detail == "auto" || imageUrl.Detail == "" {
|
||||
|
||||
@@ -59,6 +59,9 @@ export function renderNumber(num) {
|
||||
}
|
||||
|
||||
export function renderQuotaNumberWithDigit(num, digits = 2) {
|
||||
if (typeof num !== 'number' || isNaN(num)) {
|
||||
return 0;
|
||||
}
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
num = num.toFixed(digits);
|
||||
if (displayInCurrency) {
|
||||
|
||||
@@ -180,6 +180,9 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') {
|
||||
let month = (date.getMonth() + 1).toString();
|
||||
let day = date.getDate().toString();
|
||||
let hour = date.getHours().toString();
|
||||
if (day === '24') {
|
||||
console.log("timestamp", timestamp);
|
||||
}
|
||||
if (month.length === 1) {
|
||||
month = '0' + month;
|
||||
}
|
||||
|
||||
@@ -143,8 +143,7 @@ const Detail = (props) => {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['Model'],
|
||||
value: (datum) =>
|
||||
renderQuotaNumberWithDigit(parseFloat(datum['Usage']), 4),
|
||||
value: (datum) => renderQuota(datum['rawQuota'] || 0, 4),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -152,22 +151,28 @@ const Detail = (props) => {
|
||||
content: [
|
||||
{
|
||||
key: (datum) => datum['Model'],
|
||||
value: (datum) => datum['Usage'],
|
||||
value: (datum) => datum['rawQuota'] || 0,
|
||||
},
|
||||
],
|
||||
updateContent: (array) => {
|
||||
array.sort((a, b) => b.value - a.value);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
sum += parseFloat(array[i].value);
|
||||
array[i].value = renderQuotaNumberWithDigit(
|
||||
parseFloat(array[i].value),
|
||||
4,
|
||||
);
|
||||
if (array[i].key == "其他") {
|
||||
continue;
|
||||
}
|
||||
let value = parseFloat(array[i].value);
|
||||
if (isNaN(value)) {
|
||||
value = 0;
|
||||
}
|
||||
if (array[i].datum && array[i].datum.TimeSum) {
|
||||
sum = array[i].datum.TimeSum;
|
||||
}
|
||||
array[i].value = renderQuota(value, 4);
|
||||
}
|
||||
array.unshift({
|
||||
key: t('总计'),
|
||||
value: renderQuotaNumberWithDigit(sum, 4),
|
||||
value: renderQuota(sum, 4),
|
||||
});
|
||||
return array;
|
||||
},
|
||||
@@ -212,19 +217,8 @@ const Detail = (props) => {
|
||||
created_at: now.getTime() / 1000,
|
||||
});
|
||||
}
|
||||
// 根据dataExportDefaultTime重制时间粒度
|
||||
let timeGranularity = 3600;
|
||||
if (dataExportDefaultTime === 'day') {
|
||||
timeGranularity = 86400;
|
||||
} else if (dataExportDefaultTime === 'week') {
|
||||
timeGranularity = 604800;
|
||||
}
|
||||
// sort created_at
|
||||
data.sort((a, b) => a.created_at - b.created_at);
|
||||
data.forEach((item) => {
|
||||
item['created_at'] =
|
||||
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
|
||||
});
|
||||
updateChartData(data);
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -250,14 +244,14 @@ const Detail = (props) => {
|
||||
let uniqueModels = new Set();
|
||||
let totalTokens = 0;
|
||||
|
||||
// 收集所有唯一的模型名称和时间点
|
||||
let uniqueTimes = new Set();
|
||||
// 收集所有唯一的模型名称
|
||||
data.forEach(item => {
|
||||
uniqueModels.add(item.model_name);
|
||||
uniqueTimes.add(timestamp2string1(item.created_at, dataExportDefaultTime));
|
||||
totalTokens += item.token_used;
|
||||
totalQuota += item.quota;
|
||||
totalTimes += item.count;
|
||||
});
|
||||
|
||||
|
||||
// 处理颜色映射
|
||||
const newModelColors = {};
|
||||
Array.from(uniqueModels).forEach((modelName) => {
|
||||
@@ -267,56 +261,82 @@ const Detail = (props) => {
|
||||
});
|
||||
setModelColors(newModelColors);
|
||||
|
||||
// 处理饼图数据
|
||||
for (let item of data) {
|
||||
totalQuota += item.quota;
|
||||
totalTimes += item.count;
|
||||
|
||||
let pieItem = newPieData.find((it) => it.type === item.model_name);
|
||||
if (pieItem) {
|
||||
pieItem.value += item.count;
|
||||
} else {
|
||||
newPieData.push({
|
||||
type: item.model_name,
|
||||
value: item.count,
|
||||
// 按时间和模型聚合数据
|
||||
let aggregatedData = new Map();
|
||||
data.forEach(item => {
|
||||
const timeKey = timestamp2string1(item.created_at, dataExportDefaultTime);
|
||||
const modelKey = item.model_name;
|
||||
const key = `${timeKey}-${modelKey}`;
|
||||
|
||||
if (!aggregatedData.has(key)) {
|
||||
aggregatedData.set(key, {
|
||||
time: timeKey,
|
||||
model: modelKey,
|
||||
quota: 0,
|
||||
count: 0
|
||||
});
|
||||
}
|
||||
|
||||
const existing = aggregatedData.get(key);
|
||||
existing.quota += item.quota;
|
||||
existing.count += item.count;
|
||||
});
|
||||
|
||||
// 处理饼图数据
|
||||
let modelTotals = new Map();
|
||||
for (let [_, value] of aggregatedData) {
|
||||
if (!modelTotals.has(value.model)) {
|
||||
modelTotals.set(value.model, 0);
|
||||
}
|
||||
modelTotals.set(value.model, modelTotals.get(value.model) + value.count);
|
||||
}
|
||||
|
||||
// 处理柱状图数据
|
||||
let timePoints = Array.from(uniqueTimes);
|
||||
newPieData = Array.from(modelTotals).map(([model, count]) => ({
|
||||
type: model,
|
||||
value: count
|
||||
}));
|
||||
|
||||
// 生成时间点序列
|
||||
let timePoints = Array.from(new Set([...aggregatedData.values()].map(d => d.time)));
|
||||
if (timePoints.length < 7) {
|
||||
// 根据时间粒度生成合适的时间点
|
||||
const generateTimePoints = () => {
|
||||
let lastTime = Math.max(...data.map(item => item.created_at));
|
||||
let points = [];
|
||||
let interval = dataExportDefaultTime === 'hour' ? 3600
|
||||
const lastTime = Math.max(...data.map(item => item.created_at));
|
||||
const interval = dataExportDefaultTime === 'hour' ? 3600
|
||||
: dataExportDefaultTime === 'day' ? 86400
|
||||
: 604800;
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
points.push(timestamp2string1(lastTime - (i * interval), dataExportDefaultTime));
|
||||
}
|
||||
return points.reverse();
|
||||
};
|
||||
|
||||
timePoints = generateTimePoints();
|
||||
|
||||
timePoints = Array.from({length: 7}, (_, i) =>
|
||||
timestamp2string1(lastTime - (6-i) * interval, dataExportDefaultTime)
|
||||
);
|
||||
}
|
||||
|
||||
// 为每个时间点和模型生成数据
|
||||
// 生成柱状图数据
|
||||
timePoints.forEach(time => {
|
||||
Array.from(uniqueModels).forEach(model => {
|
||||
let existingData = data.find(item =>
|
||||
timestamp2string1(item.created_at, dataExportDefaultTime) === time &&
|
||||
item.model_name === model
|
||||
);
|
||||
|
||||
newLineData.push({
|
||||
// 为每个时间点收集所有模型的数据
|
||||
let timeData = Array.from(uniqueModels).map(model => {
|
||||
const key = `${time}-${model}`;
|
||||
const aggregated = aggregatedData.get(key);
|
||||
return {
|
||||
Time: time,
|
||||
Model: model,
|
||||
Usage: existingData ? parseFloat(getQuotaWithUnit(existingData.quota)) : 0
|
||||
});
|
||||
rawQuota: aggregated?.quota || 0,
|
||||
Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0
|
||||
};
|
||||
});
|
||||
|
||||
// 计算该时间点的总计
|
||||
const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0);
|
||||
|
||||
// 按照 rawQuota 从大到小排序
|
||||
timeData.sort((a, b) => b.rawQuota - a.rawQuota);
|
||||
|
||||
// 为每个数据点添加该时间的总计
|
||||
timeData = timeData.map(item => ({
|
||||
...item,
|
||||
TimeSum: timeSum
|
||||
}));
|
||||
|
||||
// 将排序后的数据添加到 newLineData
|
||||
newLineData.push(...timeData);
|
||||
});
|
||||
|
||||
// 排序
|
||||
|
||||
Reference in New Issue
Block a user