Compare commits

..

14 Commits

Author SHA1 Message Date
CaIon
fd6838e690 feat: enable error logging configuration in docker-compose and application 2025-04-29 16:26:55 +08:00
CaIon
b64480b750 fix: gemini thinking tokens count #1014 2025-04-29 16:21:54 +08:00
CaIon
da6423de33 refactor: Reducing the lock duration to the minimum necessary time in CacheGetRandomSatisfiedChannel function 2025-04-29 15:57:21 +08:00
CaIon
fe37718259 fix: update audio ratio logic for model names in GetAudioRatio function 2025-04-28 20:55:40 +08:00
IcedTangerine
c412fd9cde Merge pull request #1008 from JoeyLearnsToCode/feat-search-channel-by-url
feat: support searching channels by base url
2025-04-28 13:15:49 +08:00
creamlike1024
54f5b1a951 Merge branch 'wzxjohn-feature/wellknown' 2025-04-28 12:55:06 +08:00
JoeyLearnsToCode
a9b9d23586 feat: support searching channels by base url 2025-04-28 11:38:53 +08:00
wzxjohn
168226ba10 fix: remove custom header in oidc well known request 2025-04-28 11:25:04 +08:00
wzxjohn
1a8fd61a98 feat: support empty well known url 2025-04-28 11:25:04 +08:00
wzxjohn
2bd2d73d33 feat: improve log delete api 2025-04-28 11:25:04 +08:00
creamlike1024
62da481dc6 Merge branch 'error-logs' of github.com:zenghongtu/new-api into zenghongtu-error-logs 2025-04-28 11:06:32 +08:00
CaIon
4217358de7 feat: add image preview functionality and update model name instructions in EditChannel 2025-04-27 17:20:49 +08:00
CaIon
bb9f5a4a6d refactor: rename InitModelSettings to InitRatioSettings 2025-04-26 17:15:34 +08:00
jasonzeng
97bc2b4474 feat: add error logging functionality to relay and update logs table for error type display 2025-04-12 00:43:34 +08:00
18 changed files with 175 additions and 43 deletions

View File

@@ -16,6 +16,7 @@ var GeminiVisionMaxImageNum int
var NotifyLimitCount int
var NotificationLimitDurationMinute int
var GenerateDefaultToken bool
var ErrorLogEnabled bool
//var GeminiModelMap = map[string]string{
// "gemini-1.0-pro": "v1",
@@ -36,6 +37,8 @@ func InitEnv() {
NotificationLimitDurationMinute = common.GetEnvOrDefault("NOTIFICATION_LIMIT_DURATION_MINUTE", 10)
// GenerateDefaultToken 是否生成初始令牌,默认关闭。
GenerateDefaultToken = common.GetEnvOrDefaultBool("GENERATE_DEFAULT_TOKEN", false)
// 是否启用错误日志
ErrorLogEnabled = common.GetEnvOrDefaultBool("ERROR_LOG_ENABLED", false)
//modelVersionMapStr := strings.TrimSpace(os.Getenv("GEMINI_MODEL_MAP"))
//if modelVersionMapStr == "" {

View File

@@ -196,7 +196,7 @@ func DeleteHistoryLogs(c *gin.Context) {
})
return
}
count, err := model.DeleteOldLog(targetTimestamp)
count, err := model.DeleteOldLog(c.Request.Context(), targetTimestamp, 100)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,

View File

@@ -10,6 +10,7 @@ import (
"log"
"net/http"
"one-api/common"
constant2 "one-api/constant"
"one-api/dto"
"one-api/middleware"
"one-api/model"
@@ -39,6 +40,26 @@ func relayHandler(c *gin.Context, relayMode int) *dto.OpenAIErrorWithStatusCode
default:
err = relay.TextHelper(c)
}
if constant2.ErrorLogEnabled && err != nil {
// 保存错误日志到mysql中
userId := c.GetInt("id")
tokenName := c.GetString("token_name")
modelName := c.GetString("original_model")
tokenId := c.GetInt("token_id")
userGroup := c.GetString("group")
channelId := c.GetInt("channel_id")
other := make(map[string]interface{})
other["error_type"] = err.Error.Type
other["error_code"] = err.Error.Code
other["status_code"] = err.StatusCode
other["channel_id"] = channelId
other["channel_name"] = c.GetString("channel_name")
other["channel_type"] = c.GetInt("channel_type")
model.RecordErrorLog(c, userId, channelId, modelName, tokenName, err.Error.Message, tokenId, 0, false, userGroup, other)
}
return err
}

View File

@@ -15,6 +15,7 @@ services:
- SQL_DSN=root:123456@tcp(mysql:3306)/new-api # Point to the mysql service
- REDIS_CONN_STRING=redis://redis
- TZ=Asia/Shanghai
- ERROR_LOG_ENABLED=true # 是否启用错误日志记录
# - TIKTOKEN_CACHE_DIR=./tiktoken_cache # 如果需要使用tiktoken_cache请取消注释
# - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!!!!!!!
# - NODE_TYPE=slave # Uncomment for slave node in multi-node deployment

View File

@@ -51,6 +51,7 @@ type GeneralOpenAIRequest struct {
Dimensions int `json:"dimensions,omitempty"`
Modalities any `json:"modalities,omitempty"`
Audio any `json:"audio,omitempty"`
EnableThinking any `json:"enable_thinking,omitempty"` // ali
ExtraBody any `json:"extra_body,omitempty"`
}

View File

@@ -74,7 +74,7 @@ func main() {
}
// Initialize model settings
operation_setting.InitModelSettings()
operation_setting.InitRatioSettings()
// Initialize constants
constant.InitEnv()
// Initialize options

View File

@@ -84,9 +84,11 @@ func CacheGetRandomSatisfiedChannel(group string, model string, retry int) (*Cha
if !common.MemoryCacheEnabled {
return GetRandomSatisfiedChannel(group, model, retry)
}
channelSyncLock.RLock()
defer channelSyncLock.RUnlock()
channels := group2model2channels[group][model]
channelSyncLock.RUnlock()
if len(channels) == 0 {
return nil, errors.New("channel not found")
}

View File

@@ -119,10 +119,15 @@ func SearchChannels(keyword string, group string, model string, idSort bool) ([]
// 如果是 PostgreSQL使用双引号
if common.UsingPostgreSQL {
keyCol = `"key"`
modelsCol = `"models"`
}
baseURLCol := "`base_url`"
// 如果是 PostgreSQL使用双引号
if common.UsingPostgreSQL {
baseURLCol = `"base_url"`
}
order := "priority desc"
if idSort {
order = "id desc"
@@ -142,11 +147,11 @@ func SearchChannels(keyword string, group string, model string, idSort bool) ([]
// sqlite, PostgreSQL
groupCondition = `(',' || ` + groupCol + ` || ',') LIKE ?`
}
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + modelsCol + ` LIKE ? AND ` + groupCondition
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%", "%,"+group+",%")
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ? OR " + baseURLCol + " LIKE ?) AND " + modelsCol + ` LIKE ? AND ` + groupCondition
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+keyword+"%", "%"+model+"%", "%,"+group+",%")
} else {
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + modelsCol + " LIKE ?"
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%")
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ? OR " + baseURLCol + " LIKE ?) AND " + modelsCol + " LIKE ?"
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+keyword+"%", "%"+model+"%")
}
// 执行查询
@@ -450,6 +455,12 @@ func SearchTags(keyword string, group string, model string, idSort bool) ([]*str
modelsCol = `"models"`
}
baseURLCol := "`base_url`"
// 如果是 PostgreSQL使用双引号
if common.UsingPostgreSQL {
baseURLCol = `"base_url"`
}
order := "priority desc"
if idSort {
order = "id desc"
@@ -469,11 +480,11 @@ func SearchTags(keyword string, group string, model string, idSort bool) ([]*str
// sqlite, PostgreSQL
groupCondition = `(',' || ` + groupCol + ` || ',') LIKE ?`
}
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + modelsCol + ` LIKE ? AND ` + groupCondition
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%", "%,"+group+",%")
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ? OR " + baseURLCol + " LIKE ?) AND " + modelsCol + ` LIKE ? AND ` + groupCondition
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+keyword+"%", "%"+model+"%", "%,"+group+",%")
} else {
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ?) AND " + modelsCol + " LIKE ?"
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+model+"%")
whereClause = "(id = ? OR name LIKE ? OR " + keyCol + " = ? OR " + baseURLCol + " LIKE ?) AND " + modelsCol + " LIKE ?"
args = append(args, common.String2Int(keyword), "%"+keyword+"%", keyword, "%"+keyword+"%", "%"+model+"%")
}
subQuery := baseQuery.Where(whereClause, args...).

View File

@@ -1,6 +1,7 @@
package model
import (
"context"
"fmt"
"one-api/common"
"os"
@@ -40,6 +41,7 @@ const (
LogTypeConsume
LogTypeManage
LogTypeSystem
LogTypeError
)
func formatUserLogs(logs []*Log) {
@@ -88,6 +90,35 @@ func RecordLog(userId int, logType int, content string) {
}
}
func RecordErrorLog(c *gin.Context, userId int, channelId int, modelName string, tokenName string, content string, tokenId int, useTimeSeconds int,
isStream bool, group string, other map[string]interface{}) {
common.LogInfo(c, fmt.Sprintf("record error log: userId=%d, channelId=%d, modelName=%s, tokenName=%s, content=%s", userId, channelId, modelName, tokenName, content))
username := c.GetString("username")
otherStr := common.MapToJsonStr(other)
log := &Log{
UserId: userId,
Username: username,
CreatedAt: common.GetTimestamp(),
Type: LogTypeError,
Content: content,
PromptTokens: 0,
CompletionTokens: 0,
TokenName: tokenName,
ModelName: modelName,
Quota: 0,
ChannelId: channelId,
TokenId: tokenId,
UseTime: useTimeSeconds,
IsStream: isStream,
Group: group,
Other: otherStr,
}
err := LOG_DB.Create(log).Error
if err != nil {
common.LogError(c, "failed to record log: "+err.Error())
}
}
func RecordConsumeLog(c *gin.Context, userId int, channelId int, promptTokens int, completionTokens int,
modelName string, tokenName string, quota int, content string, tokenId int, userQuota int, useTimeSeconds int,
isStream bool, group string, other map[string]interface{}) {
@@ -310,7 +341,25 @@ func SumUsedToken(logType int, startTimestamp int64, endTimestamp int64, modelNa
return token
}
func DeleteOldLog(targetTimestamp int64) (int64, error) {
result := LOG_DB.Where("created_at < ?", targetTimestamp).Delete(&Log{})
return result.RowsAffected, result.Error
func DeleteOldLog(ctx context.Context, targetTimestamp int64, limit int) (int64, error) {
var total int64 = 0
for {
if nil != ctx.Err() {
return total, ctx.Err()
}
result := LOG_DB.Where("created_at < ?", targetTimestamp).Limit(limit).Delete(&Log{})
if nil != result.Error {
return total, result.Error
}
total += result.RowsAffected
if result.RowsAffected < int64(limit) {
break
}
}
return total, nil
}

View File

@@ -1,7 +1,12 @@
package ali
var ModelList = []string{
"qwen-turbo", "qwen-plus", "qwen-max", "qwen-max-longcontext",
"qwen-turbo",
"qwen-plus",
"qwen-max",
"qwen-max-longcontext",
"qwq-32b",
"qwen3-235b-a22b",
"text-embedding-v1",
}

View File

@@ -670,6 +670,7 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
usage.PromptTokens = geminiResponse.UsageMetadata.PromptTokenCount
usage.CompletionTokens = geminiResponse.UsageMetadata.CandidatesTokenCount
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
usage.TotalTokens = geminiResponse.UsageMetadata.TotalTokenCount
}
err = helper.ObjectData(c, response)
if err != nil {
@@ -690,9 +691,8 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom
}
}
usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens
usage.PromptTokensDetails.TextTokens = usage.PromptTokens
//usage.CompletionTokenDetails.TextTokens = usage.CompletionTokens
usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens
if info.ShouldIncludeUsage {
response = helper.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage)
@@ -740,6 +740,7 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re
}
usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount
usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens
fullTextResponse.Usage = usage
jsonResponse, err := json.Marshal(fullTextResponse)

View File

@@ -259,8 +259,8 @@ var defaultCompletionRatio = map[string]float64{
"gpt-image-1": 8,
}
// InitModelSettings initializes all model related settings maps
func InitModelSettings() {
// InitRatioSettings initializes all model related settings maps
func InitRatioSettings() {
// Initialize modelPriceMap
modelPriceMapMutex.Lock()
modelPriceMap = defaultModelPrice
@@ -517,18 +517,18 @@ func getHardcodedCompletionModelRatio(name string) (float64, bool) {
func GetAudioRatio(name string) float64 {
if strings.Contains(name, "-realtime") {
if strings.HasSuffix(name, "gpt-4o-realtime-preview-2024-12-17") {
if strings.HasSuffix(name, "gpt-4o-realtime-preview") {
return 8
} else if strings.Contains(name, "mini") {
} else if strings.Contains(name, "gpt-4o-mini-realtime-preview") {
return 10 / 0.6
} else {
return 20
}
}
if strings.Contains(name, "-audio") {
if strings.HasSuffix(name, "gpt-4o-audio-preview-2024-12-17") {
return 16
} else if strings.Contains(name, "mini") {
if strings.HasPrefix(name, "gpt-4o-audio-preview") {
return 40 / 2.5
} else if strings.HasPrefix(name, "gpt-4o-mini-audio-preview") {
return 10 / 0.15
} else {
return 40

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

View File

@@ -1276,7 +1276,7 @@ const ChannelsTable = () => {
<Form.Input
field='search_keyword'
label={t('搜索渠道关键词')}
placeholder={t('搜索渠道的 ID名称密钥 ...')}
placeholder={t('搜索渠道的 ID名称密钥和API地址 ...')}
value={searchKeyword}
loading={searching}
onChange={(v) => {

View File

@@ -103,9 +103,15 @@ const LogsTable = () => {
{t('系统')}
</Tag>
);
case 5:
return (
<Tag color='red' size='large'>
{t('错误')}
</Tag>
);
default:
return (
<Tag color='black' size='large'>
<Tag color='grey' size='large'>
{t('未知')}
</Tag>
);
@@ -373,7 +379,7 @@ const LogsTable = () => {
className: isAdmin() ? 'tableShow' : 'tableHiddle',
render: (text, record, index) => {
return isAdminUser ? (
record.type === 0 || record.type === 2 ? (
record.type === 0 || record.type === 2 || record.type === 5 ? (
<div>
{
<Tooltip content={record.channel_name || '[未知]'}>
@@ -426,7 +432,7 @@ const LogsTable = () => {
title: t('令牌'),
dataIndex: 'token_name',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
return record.type === 0 || record.type === 2 || record.type === 5 ? (
<div>
<Tag
color='grey'
@@ -450,7 +456,7 @@ const LogsTable = () => {
title: t('分组'),
dataIndex: 'group',
render: (text, record, index) => {
if (record.type === 0 || record.type === 2) {
if (record.type === 0 || record.type === 2 || record.type === 5) {
if (record.group) {
return <>{renderGroup(record.group)}</>;
} else {
@@ -490,7 +496,7 @@ const LogsTable = () => {
title: t('模型'),
dataIndex: 'model_name',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
return record.type === 0 || record.type === 2 || record.type === 5 ? (
<>{renderModelName(record)}</>
) : (
<></>
@@ -530,7 +536,7 @@ const LogsTable = () => {
title: t('提示'),
dataIndex: 'prompt_tokens',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
return record.type === 0 || record.type === 2 || record.type === 5 ? (
<>{<span> {text} </span>}</>
) : (
<></>
@@ -543,7 +549,7 @@ const LogsTable = () => {
dataIndex: 'completion_tokens',
render: (text, record, index) => {
return parseInt(text) > 0 &&
(record.type === 0 || record.type === 2) ? (
(record.type === 0 || record.type === 2 || record.type === 5) ? (
<>{<span> {text} </span>}</>
) : (
<></>
@@ -555,7 +561,7 @@ const LogsTable = () => {
title: t('花费'),
dataIndex: 'quota',
render: (text, record, index) => {
return record.type === 0 || record.type === 2 ? (
return record.type === 0 || record.type === 2 || record.type === 5 ? (
<>{renderQuota(text, 6)}</>
) : (
<></>
@@ -986,9 +992,10 @@ const LogsTable = () => {
other?.group_ratio,
other?.cache_tokens || 0,
other?.cache_ratio || 1.0,
other?.image || false,
other?.image_ratio || 0,
other?.image_output || 0,);
other?.image || false,
other?.image_ratio || 0,
other?.image_output || 0,
);
}
expandDataLocal.push({
key: t('计费过程'),
@@ -1247,6 +1254,7 @@ const LogsTable = () => {
<Select.Option value='2'>{t('消费')}</Select.Option>
<Select.Option value='3'>{t('管理')}</Select.Option>
<Select.Option value='4'>{t('系统')}</Select.Option>
<Select.Option value='5'>{t('错误')}</Select.Option>
</Select>
<Button
theme='light'

View File

@@ -19,6 +19,7 @@ import {
verifyJSON,
} from '../helpers/utils';
import { API } from '../helpers/api';
import axios from "axios";
const SystemSetting = () => {
let [inputs, setInputs] = useState({
@@ -374,7 +375,7 @@ const SystemSetting = () => {
};
const submitOIDCSettings = async () => {
if (inputs['oidc.well_known'] !== '') {
if (inputs['oidc.well_known'] && inputs['oidc.well_known'] !== '') {
if (
!inputs['oidc.well_known'].startsWith('http://') &&
!inputs['oidc.well_known'].startsWith('https://')
@@ -383,7 +384,7 @@ const SystemSetting = () => {
return;
}
try {
const res = await API.get(inputs['oidc.well_known']);
const res = await axios.create().get(inputs['oidc.well_known']);
inputs['oidc.authorization_endpoint'] =
res.data['authorization_endpoint'];
inputs['oidc.token_endpoint'] = res.data['token_endpoint'];

View File

@@ -124,7 +124,7 @@
"已成功开始测试所有已启用通道,请刷新页面查看结果。": "Successfully started testing all enabled channels. Please refresh page to view results.",
"通道 ${name} 余额更新成功!": "Channel ${name} quota updated successfully!",
"已更新完毕所有已启用通道余额!": "Updated quota for all enabled channels!",
"搜索渠道的 ID名称密钥 ...": "Search channel ID, name and key...",
"搜索渠道的 ID名称密钥和API地址 ...": "Search channel ID, name, key and Base URL...",
"名称": "Name",
"分组": "Group",
"类型": "Type",
@@ -492,7 +492,7 @@
"请输入默认 API 版本例如2023-03-15-preview该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters",
"默认": "default",
"图片演示": "Image demo",
"注意系统请求的时模型名称中的点会被剔除例如gpt-4.5-preview会请求为gpt-45-preview所以部署的模型名称需要去掉点": "Note that the dot in the model name requested by the system will be removed, for example: gpt-4.5-preview will be requested as gpt-45-preview, so the deployed model name needs to remove the dot",
"注意系统请求的时模型名称中的点会被剔除例如gpt-4.1会请求为gpt-41所以在Azure部署的时候部署模型名称需要手动改为gpt-41": "Note that the dot in the model name requested by the system will be removed, for example: gpt-4.1 will be requested as gpt-41, so when deploying on Azure, the deployment model name needs to be manually changed to gpt-41",
"模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!",
"取消无限额度": "Cancel unlimited quota",
"取消": "Cancel",

View File

@@ -24,9 +24,10 @@ import {
TextArea,
Checkbox,
Banner,
Modal,
Modal, ImagePreview
} from '@douyinfe/semi-ui';
import { getChannelModels, loadChannelModels } from '../../components/utils.js';
import { IconHelpCircle } from '@douyinfe/semi-icons';
const MODEL_MAPPING_EXAMPLE = {
'gpt-3.5-turbo': 'gpt-3.5-turbo-0125',
@@ -96,6 +97,8 @@ const EditChannel = (props) => {
const [basicModels, setBasicModels] = useState([]);
const [fullModels, setFullModels] = useState([]);
const [customModel, setCustomModel] = useState('');
const [modalImageUrl, setModalImageUrl] = useState('');
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
const handleInputChange = (name, value) => {
if (name === 'base_url' && value.endsWith('/v1')) {
Modal.confirm({
@@ -472,7 +475,28 @@ const EditChannel = (props) => {
<div style={{ marginTop: 10 }}>
<Banner
type={'warning'}
description={t('注意系统请求的时模型名称中的点会被剔除例如gpt-4.5-preview会请求为gpt-45-preview所以部署的模型名称需要去掉点')}
description={
<>
{t('注意系统请求的时模型名称中的点会被剔除例如gpt-4.1会请求为gpt-41所以在Azure部署的时候部署模型名称需要手动改为gpt-41')}
<br />
<Typography.Text
style={{
color: 'rgba(var(--semi-blue-5), 1)',
userSelect: 'none',
cursor: 'pointer',
}}
onClick={() => {
setModalImageUrl(
'/azure_model_name.png',
);
setIsModalOpenurl(true)
}}
>
{t('查看示例')}
</Typography.Text>
</>
}
></Banner>
</div>
<div style={{ marginTop: 10 }}>
@@ -1109,6 +1133,11 @@ const EditChannel = (props) => {
{t('填入模板')}
</Typography.Text>
</Spin>
<ImagePreview
src={modalImageUrl}
visible={isModalOpenurl}
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
/>
</SideSheet>
</>
);