Compare commits

..

3 Commits

Author SHA1 Message Date
1808837298@qq.com
d042a1bd55 refactor: Improve channel testing and model price handling 2025-03-02 15:47:12 +08:00
1808837298@qq.com
816e831a2e feat: Persist models expanded state in PersonalSetting component 2025-03-02 01:35:50 +08:00
1808837298@qq.com
a3ceae4a86 feat: Enhance update checking and system information display
- Add version and startup time display in OtherSetting component
- Implement robust GitHub release update checking mechanism
- Add error handling for update check process
- Update Modal component for displaying update information
- Add new translations for version and system information
2025-03-02 01:31:27 +08:00
5 changed files with 152 additions and 75 deletions

View File

@@ -17,8 +17,8 @@ import (
"one-api/relay"
relaycommon "one-api/relay/common"
"one-api/relay/constant"
"one-api/relay/helper"
"one-api/service"
"one-api/setting"
"strconv"
"strings"
"sync"
@@ -73,18 +73,6 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
}
}
modelMapping := *channel.ModelMapping
if modelMapping != "" && modelMapping != "{}" {
modelMap := make(map[string]string)
err := json.Unmarshal([]byte(modelMapping), &modelMap)
if err != nil {
return err, service.OpenAIErrorWrapperLocal(err, "unmarshal_model_mapping_failed", http.StatusInternalServerError)
}
if modelMap[testModel] != "" {
testModel = modelMap[testModel]
}
}
cache, err := model.GetUserCache(1)
if err != nil {
return err, nil
@@ -98,7 +86,13 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
middleware.SetupContextForSelectedChannel(c, channel, testModel)
meta := relaycommon.GenRelayInfo(c)
info := relaycommon.GenRelayInfo(c)
err = helper.ModelMappedHelper(c, info)
if err != nil {
return err, nil
}
apiType, _ := constant.ChannelType2APIType(channel.Type)
adaptor := relay.GetAdaptor(apiType)
if adaptor == nil {
@@ -106,12 +100,12 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
}
request := buildTestRequest(testModel)
meta.UpstreamModelName = testModel
common.SysLog(fmt.Sprintf("testing channel %d with model %s , meta %v ", channel.Id, testModel, meta))
info.OriginModelName = testModel
common.SysLog(fmt.Sprintf("testing channel %d with model %s , info %v ", channel.Id, testModel, info))
adaptor.Init(meta)
adaptor.Init(info)
convertedRequest, err := adaptor.ConvertRequest(c, meta, request)
convertedRequest, err := adaptor.ConvertRequest(c, info, request)
if err != nil {
return err, nil
}
@@ -121,7 +115,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
}
requestBody := bytes.NewBuffer(jsonData)
c.Request.Body = io.NopCloser(requestBody)
resp, err := adaptor.DoRequest(c, meta, requestBody)
resp, err := adaptor.DoRequest(c, info, requestBody)
if err != nil {
return err, nil
}
@@ -133,7 +127,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
return fmt.Errorf("status code %d: %s", httpResp.StatusCode, err.Error.Message), err
}
}
usageA, respErr := adaptor.DoResponse(c, httpResp, meta)
usageA, respErr := adaptor.DoResponse(c, httpResp, info)
if respErr != nil {
return fmt.Errorf("%s", respErr.Error.Message), respErr
}
@@ -146,27 +140,24 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr
if err != nil {
return err, nil
}
modelPrice, usePrice := setting.GetModelPrice(testModel, false)
modelRatio, success := setting.GetModelRatio(testModel)
if !usePrice && !success {
return fmt.Errorf("模型 %s 倍率和价格均未设置,请设置或者开启自用模式", testModel), nil
priceData, err := helper.ModelPriceHelper(c, info, usage.PromptTokens, int(request.MaxTokens))
if err != nil {
return err, nil
}
completionRatio := setting.GetCompletionRatio(testModel)
ratio := modelRatio
quota := 0
if !usePrice {
quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*completionRatio))
quota = int(math.Round(float64(quota) * ratio))
if ratio != 0 && quota <= 0 {
if !priceData.UsePrice {
quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*priceData.CompletionRatio))
quota = int(math.Round(float64(quota) * priceData.ModelRatio))
if priceData.ModelRatio != 0 && quota <= 0 {
quota = 1
}
} else {
quota = int(modelPrice * common.QuotaPerUnit)
quota = int(priceData.ModelPrice * common.QuotaPerUnit)
}
tok := time.Now()
milliseconds := tok.Sub(tik).Milliseconds()
consumedTime := float64(milliseconds) / 1000.0
other := service.GenerateTextOtherInfo(c, meta, modelRatio, 1, completionRatio, modelPrice)
other := service.GenerateTextOtherInfo(c, info, priceData.ModelRatio, priceData.GroupRatio, priceData.CompletionRatio, priceData.ModelPrice)
model.RecordConsumeLog(c, 1, channel.Id, usage.PromptTokens, usage.CompletionTokens, testModel, "模型测试",
quota, "模型测试", 0, quota, int(consumedTime), false, "default", other)
common.SysLog(fmt.Sprintf("testing channel #%d, response: \n%s", channel.Id, string(respBody)))

View File

@@ -11,6 +11,7 @@ import (
type PriceData struct {
ModelPrice float64
ModelRatio float64
CompletionRatio float64
GroupRatio float64
UsePrice bool
ShouldPreConsumedQuota int
@@ -21,6 +22,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
groupRatio := setting.GetGroupRatio(info.Group)
var preConsumedQuota int
var modelRatio float64
var completionRatio float64
if !usePrice {
preConsumedTokens := common.PreConsumedQuota
if maxTokens != 0 {
@@ -35,6 +37,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置, 请联系管理员设置Model %s ratio or price not set, please contact administrator to set", info.OriginModelName, info.OriginModelName)
}
}
completionRatio = setting.GetCompletionRatio(info.OriginModelName)
ratio := modelRatio * groupRatio
preConsumedQuota = int(float64(preConsumedTokens) * ratio)
} else {
@@ -43,6 +46,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens
return PriceData{
ModelPrice: modelPrice,
ModelRatio: modelRatio,
CompletionRatio: completionRatio,
GroupRatio: groupRatio,
UsePrice: usePrice,
ShouldPreConsumedQuota: preConsumedQuota,

View File

@@ -1,8 +1,10 @@
import React, { useEffect, useRef, useState } from 'react';
import { Banner, Button, Col, Form, Row } from '@douyinfe/semi-ui';
import { API, showError, showSuccess } from '../helpers';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { Banner, Button, Col, Form, Row, Modal, Space } from '@douyinfe/semi-ui';
import { API, showError, showSuccess, timestamp2string } from '../helpers';
import { marked } from 'marked';
import { useTranslation } from 'react-i18next';
import { StatusContext } from '../context/Status/index.js';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
const OtherSetting = () => {
const { t } = useTranslation();
@@ -16,6 +18,7 @@ const OtherSetting = () => {
});
let [loading, setLoading] = useState(false);
const [showUpdateModal, setShowUpdateModal] = useState(false);
const [statusState, statusDispatch] = useContext(StatusContext);
const [updateData, setUpdateData] = useState({
tag_name: '',
content: '',
@@ -43,6 +46,7 @@ const OtherSetting = () => {
HomePageContent: false,
About: false,
Footer: false,
CheckUpdate: false
});
const handleInputChange = async (value, e) => {
const name = e.target.id;
@@ -145,23 +149,48 @@ const OtherSetting = () => {
}
};
const openGitHubRelease = () => {
window.location = 'https://github.com/songquanpeng/one-api/releases/latest';
};
const checkUpdate = async () => {
const res = await API.get(
'https://api.github.com/repos/songquanpeng/one-api/releases/latest',
);
const { tag_name, body } = res.data;
if (tag_name === process.env.REACT_APP_VERSION) {
showSuccess(`已是最新版本:${tag_name}`);
} else {
setUpdateData({
tag_name: tag_name,
content: marked.parse(body),
});
setShowUpdateModal(true);
try {
setLoadingInput((loadingInput) => ({ ...loadingInput, CheckUpdate: true }));
// Use a CORS proxy to avoid direct cross-origin requests to GitHub API
// Option 1: Use a public CORS proxy service
// const proxyUrl = 'https://cors-anywhere.herokuapp.com/';
// const res = await API.get(
// `${proxyUrl}https://api.github.com/repos/Calcium-Ion/new-api/releases/latest`,
// );
// Option 2: Use the JSON proxy approach which often works better with GitHub API
const res = await fetch(
'https://api.github.com/repos/Calcium-Ion/new-api/releases/latest',
{
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
// Adding User-Agent which is often required by GitHub API
'User-Agent': 'new-api-update-checker'
}
}
).then(response => response.json());
// Option 3: Use a local proxy endpoint
// Create a cached version of the response to avoid frequent GitHub API calls
// const res = await API.get('/api/status/github-latest-release');
const { tag_name, body } = res;
if (tag_name === statusState?.status?.version) {
showSuccess(`已是最新版本:${tag_name}`);
} else {
setUpdateData({
tag_name: tag_name,
content: marked.parse(body),
});
setShowUpdateModal(true);
}
} catch (error) {
console.error('Failed to check for updates:', error);
showError('检查更新失败,请稍后再试');
} finally {
setLoadingInput((loadingInput) => ({ ...loadingInput, CheckUpdate: false }));
}
};
const getOptions = async () => {
@@ -186,9 +215,41 @@ const OtherSetting = () => {
getOptions();
}, []);
// Function to open GitHub release page
const openGitHubRelease = () => {
window.open(`https://github.com/Calcium-Ion/new-api/releases/tag/${updateData.tag_name}`, '_blank');
};
const getStartTimeString = () => {
const timestamp = statusState?.status?.start_time;
return statusState.status ? timestamp2string(timestamp) : '';
};
return (
<Row>
<Col span={24}>
{/* 版本信息 */}
<Form style={{ marginBottom: 15 }}>
<Form.Section text={t('系统信息')}>
<Row>
<Col span={16}>
<Space>
<Text>
{t('当前版本')}{statusState?.status?.version || t('未知')}
</Text>
<Button type="primary" onClick={checkUpdate} loading={loadingInput['CheckUpdate']}>
{t('检查更新')}
</Button>
</Space>
</Col>
</Row>
<Row>
<Col span={16}>
<Text>{t('启动时间')}{getStartTimeString()}</Text>
</Col>
</Row>
</Form.Section>
</Form>
{/* 通用设置 */}
<Form
values={inputs}
@@ -282,28 +343,25 @@ const OtherSetting = () => {
</Form.Section>
</Form>
</Col>
{/*<Modal*/}
{/* onClose={() => setShowUpdateModal(false)}*/}
{/* onOpen={() => setShowUpdateModal(true)}*/}
{/* open={showUpdateModal}*/}
{/*>*/}
{/* <Modal.Header>新版本:{updateData.tag_name}</Modal.Header>*/}
{/* <Modal.Content>*/}
{/* <Modal.Description>*/}
{/* <div dangerouslySetInnerHTML={{ __html: updateData.content }}></div>*/}
{/* </Modal.Description>*/}
{/* </Modal.Content>*/}
{/* <Modal.Actions>*/}
{/* <Button onClick={() => setShowUpdateModal(false)}>关闭</Button>*/}
{/* <Button*/}
{/* content='详情'*/}
{/* onClick={() => {*/}
{/* setShowUpdateModal(false);*/}
{/* openGitHubRelease();*/}
{/* }}*/}
{/* />*/}
{/* </Modal.Actions>*/}
{/*</Modal>*/}
<Modal
title={t('新版本') + '' + updateData.tag_name}
visible={showUpdateModal}
onCancel={() => setShowUpdateModal(false)}
footer={[
<Button
key="details"
type="primary"
onClick={() => {
setShowUpdateModal(false);
openGitHubRelease();
}}
>
{t('详情')}
</Button>
]}
>
<div dangerouslySetInnerHTML={{ __html: updateData.content }}></div>
</Modal>
</Row>
);
};

View File

@@ -69,7 +69,11 @@ const PersonalSetting = () => {
const [models, setModels] = useState([]);
const [openTransfer, setOpenTransfer] = useState(false);
const [transferAmount, setTransferAmount] = useState(0);
const [isModelsExpanded, setIsModelsExpanded] = useState(false);
const [isModelsExpanded, setIsModelsExpanded] = useState(() => {
// Initialize from localStorage if available
const savedState = localStorage.getItem('modelsExpanded');
return savedState ? JSON.parse(savedState) : false;
});
const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量
const [notificationSettings, setNotificationSettings] = useState({
warningType: 'email',
@@ -124,6 +128,11 @@ const PersonalSetting = () => {
}
}, [userState?.user?.setting]);
// Save models expanded state to localStorage whenever it changes
useEffect(() => {
localStorage.setItem('modelsExpanded', JSON.stringify(isModelsExpanded));
}, [isModelsExpanded]);
const handleInputChange = (name, value) => {
setInputs((inputs) => ({...inputs, [name]: value}));
};

View File

@@ -1320,5 +1320,20 @@
"模型倍率和补全倍率同时设置": "Both model ratio and completion ratio are set",
"自用模式": "Self-use mode",
"开启后不限制:必须设置模型倍率": "After enabling, no limit: must set model ratio",
"演示站点模式": "Demo site mode"
"演示站点模式": "Demo site mode",
"当前版本": "Current version",
"Gemini设置": "Gemini settings",
"Gemini安全设置": "Gemini safety settings",
"default为默认设置可单独设置每个分类的安全等级": "\"default\" is the default setting, and each category can be set separately",
"Gemini版本设置": "Gemini version settings",
"default为默认设置可单独设置每个模型的版本": "\"default\" is the default setting, and each model can be set separately",
"Claude设置": "Claude settings",
"Claude请求头覆盖": "Claude request header override",
"示例": "Example",
"缺省 MaxTokens": "Default MaxTokens",
"启用Claude思考适配-thinking后缀": "Enable Claude thinking adaptation (-thinking suffix)",
"Claude思考适配 BudgetTokens = MaxTokens * BudgetTokens 百分比": "Claude thinking adaptation BudgetTokens = MaxTokens * BudgetTokens percentage",
"思考适配 BudgetTokens 百分比": "Thinking adaptation BudgetTokens percentage",
"0.1-1之间的小数": "Decimal between 0.1 and 1",
"模型相关设置": "Model related settings"
}