Compare commits

..

5 Commits

Author SHA1 Message Date
woan1136 3ab65a8221 fix: add Azure channel support for /v1/responses/compact URL routing (#4149)
The Azure channel's GetRequestURL method only handled RelayModeResponses
but missed RelayModeResponsesCompact. This caused compact requests to
fall through to the generic deployments URL pattern, producing an
incorrect path that Azure returns 404 for.

This fix extends the existing responses API special handling to also
cover the compact mode, appending /compact to the subUrl when the relay
mode is ResponsesCompact.

Affected URLs (before → after):
- Normal Azure: /openai/deployments/{model}/responses/compact → /openai/v1/responses/compact
- cognitiveservices: same pattern → /openai/responses/compact
- Custom AzureResponsesVersion: properly respected for compact too

Co-authored-by: 彭俊杰 <pengjunjie@onero.com>
2026-04-13 15:23:38 +08:00
CaIon 7cfaf6c335 feat: enhance dashboard charts with improved dimension handling and ranking logic 2026-04-13 15:12:12 +08:00
MS 2bedd31b42 feat: display next quota reset time in subscription card (#4181)
Show the next quota reset time for active subscriptions in the "My Subscriptions"
section when a reset period is configured (next_reset_time > 0). Hidden when
the subscription plan has no quota reset configured.
2026-04-13 14:48:32 +08:00
萧邦 c20060931b fix(GroupTable): prevent Input cursor jumping to end on keystroke (#4208)
Refactor updateRow/addRow/removeRow to use functional setRows(prev => ...)
and ref-based onChange/duplicateNames access, making columns useMemo stable
across keystrokes so Semi UI Table does not re-mount Input components.
2026-04-13 14:41:40 +08:00
CaIon 8b22161527 fix: set TopP to nil in Claude request configuration 2026-04-13 14:36:22 +08:00
12 changed files with 132 additions and 47 deletions
+1 -1
View File
@@ -160,7 +160,7 @@ func RequestOpenAI2ClaudeMessage(c *gin.Context, textRequest dto.GeneralOpenAIRe
Type: "adaptive",
}
claudeRequest.OutputConfig = json.RawMessage(fmt.Sprintf(`{"effort":"%s"}`, effortLevel))
claudeRequest.TopP = common.GetPointer[float64](0)
claudeRequest.TopP = nil
claudeRequest.Temperature = common.GetPointer[float64](1.0)
} else if model_setting.GetClaudeSettings().ThinkingAdapterEnabled &&
strings.HasSuffix(textRequest.Model, "-thinking") {
+7 -2
View File
@@ -136,8 +136,8 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
task = "chat/completions" + task
}
// 特殊处理 responses API
if info.RelayMode == relayconstant.RelayModeResponses {
// 特殊处理 responses API(包含 compact
if info.RelayMode == relayconstant.RelayModeResponses || info.RelayMode == relayconstant.RelayModeResponsesCompact {
responsesApiVersion := "preview"
subUrl := "/openai/v1/responses"
@@ -150,6 +150,11 @@ func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) {
responsesApiVersion = info.ChannelOtherSettings.AzureResponsesVersion
}
// compact 模式追加 /compact
if info.RelayMode == relayconstant.RelayModeResponsesCompact {
subUrl = subUrl + "/compact"
}
requestURL = fmt.Sprintf("%s?api-version=%s", subUrl, responsesApiVersion)
return relaycommon.GetFullRequestURL(info.ChannelBaseUrl, requestURL, info.ChannelType), nil
}
@@ -442,6 +442,14 @@ const SubscriptionPlansCard = ({
(subscription?.end_time || 0) * 1000,
).toLocaleString()}
</div>
{isActive && subscription?.next_reset_time > 0 && (
<div className='text-xs text-gray-500 mb-2'>
{t('下一次重置')}:{' '}
{new Date(
subscription.next_reset_time * 1000,
).toLocaleString()}
</div>
)}
<div className='text-xs text-gray-500 mb-2'>
{t('总额度')}:{' '}
{totalAmount > 0 ? (
+57 -1
View File
@@ -214,6 +214,29 @@ export const useDashboardCharts = (
},
],
},
dimension: {
content: [
{
key: (datum) => datum['Model'],
value: (datum) => datum['Count'] || 0,
},
],
updateContent: (array) => {
array.sort((a, b) => b.value - a.value);
let sum = 0;
for (let i = 0; i < array.length; i++) {
let value = parseFloat(array[i].value);
if (isNaN(value)) value = 0;
sum += value;
array[i].value = renderNumber(value);
}
array.unshift({
key: t('总计'),
value: renderNumber(sum),
});
return array;
},
},
},
color: {
specified: modelColorMap,
@@ -335,6 +358,27 @@ export const useDashboardCharts = (
value: (datum) => renderQuota(datum['rawQuota'] || 0, 4),
}],
},
dimension: {
content: [{
key: (datum) => datum['User'],
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++) {
let value = parseFloat(array[i].value);
if (isNaN(value)) value = 0;
sum += value;
array[i].value = renderQuota(value, 4);
}
array.unshift({
key: t('总计'),
value: renderQuota(sum, 4),
});
return array;
},
},
},
color: { type: 'ordinal', range: USER_COLORS },
});
@@ -463,13 +507,25 @@ export const useDashboardCharts = (
modelLineData.sort((a, b) => a.Time.localeCompare(b.Time));
// ===== 模型调用次数排行柱状图 =====
const rankData = Array.from(modelTotals)
const MAX_RANK_MODELS = 20;
const allRankData = Array.from(modelTotals)
.map(([model, count]) => ({
Model: model,
Count: count,
}))
.sort((a, b) => b.Count - a.Count);
let rankData;
if (allRankData.length > MAX_RANK_MODELS) {
const topModels = allRankData.slice(0, MAX_RANK_MODELS);
const otherCount = allRankData
.slice(MAX_RANK_MODELS)
.reduce((sum, item) => sum + item.Count, 0);
rankData = [...topModels, { Model: t('其他'), Count: otherCount }];
} else {
rankData = allRankData;
}
updateChartSpec(
setSpecModelLine,
modelLineData,
+1
View File
@@ -440,6 +440,7 @@
"余额充值管理": "Balance recharge management",
"作废": "Invalidate",
"作废于": "Invalidated at",
"下一次重置": "Next reset",
"作废后该订阅将立即失效,历史记录不受影响。是否继续?": "After invalidation, the subscription becomes invalid immediately. History is not affected. Continue?",
"作用域": "Scope",
"作用域:包含分组": "Scope: Include Group",
+1
View File
@@ -435,6 +435,7 @@
"余额充值管理": "Recharge du solde",
"作废": "Invalider",
"作废于": "Invalidé le",
"下一次重置": "Prochaine réinitialisation",
"作废后该订阅将立即失效,历史记录不受影响。是否继续?": "Après invalidation, l'abonnement devient immédiatement invalide. L'historique n'est pas affecté. Continuer ?",
"作用域": "Portée",
"作用域:包含分组": "Portée : inclure le groupe",
+1
View File
@@ -431,6 +431,7 @@
"余额充值管理": "残高チャージ管理",
"作废": "無効化",
"作废于": "無効化日",
"下一次重置": "次回リセット",
"作废后该订阅将立即失效,历史记录不受影响。是否继续?": "無効化するとこのサブスクリプションは直ちに失効します。履歴には影響しません。続行しますか?",
"作用域": "スコープ",
"作用域:包含分组": "スコープ:グループを含む",
+1
View File
@@ -438,6 +438,7 @@
"余额充值管理": "Управление пополнением баланса",
"作废": "Аннулировать",
"作废于": "Аннулировано",
"下一次重置": "Следующий сброс",
"作废后该订阅将立即失效,历史记录不受影响。是否继续?": "После аннулирования подписка сразу станет недействительной. История не изменится. Продолжить?",
"作用域": "Область действия",
"作用域:包含分组": "Область действия: включить группу",
+1
View File
@@ -432,6 +432,7 @@
"余额充值管理": "Quản lý nạp tiền số dư",
"作废": "Vô hiệu",
"作废于": "Vô hiệu vào",
"下一次重置": "Đặt lại tiếp theo",
"作废后该订阅将立即失效,历史记录不受影响。是否继续?": "Sau khi vô hiệu, đăng ký sẽ mất hiệu lực ngay. Lịch sử không bị ảnh hưởng. Tiếp tục?",
"作用域": "Phạm vi",
"作用域:包含分组": "Phạm vi: Bao gồm nhóm",
+1
View File
@@ -2797,6 +2797,7 @@
"至": "至",
"过期于": "过期于",
"作废于": "作废于",
"下一次重置": "下一次重置",
"购买套餐后即可享受模型权益": "购买套餐后即可享受模型权益",
"限购": "限购",
"推荐": "推荐",
+1
View File
@@ -379,6 +379,7 @@
"余额充值管理": "餘額儲值管理",
"作废": "作廢",
"作废于": "作廢於",
"下一次重置": "下一次重置",
"作废后该订阅将立即失效,历史记录不受影响。是否继续?": "作廢後該訂閱將立即失效,歷史記錄不受影響。是否繼續?",
"你似乎并没有修改什么": "你似乎並沒有修改什麼",
"你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。": "你可以在「自訂模型名稱」處手動添加它們,然後點擊填入後再提交,或者直接使用下方操作自動處理。",
@@ -1,4 +1,4 @@
import React, { useState, useCallback, useMemo } from 'react';
import React, { useState, useCallback, useMemo, useRef } from 'react';
import {
Button,
Input,
@@ -61,60 +61,63 @@ export function serializeGroupTable(rows) {
};
}
export default function GroupTable({
groupRatio,
userUsableGroups,
onChange,
}) {
export default function GroupTable({ groupRatio, userUsableGroups, onChange }) {
const { t } = useTranslation();
const [rows, setRows] = useState(() =>
buildRows(groupRatio, userUsableGroups),
);
const emitChange = useCallback(
(newRows) => {
setRows(newRows);
onChange?.(serializeGroupTable(newRows));
},
[onChange],
);
// Use functional setRows to keep updateRow/addRow/removeRow referentially
// stable, preventing columns useMemo from rebuilding on every keystroke
// which causes the Input cursor to jump to end (cursor reset bug).
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
const emitAndSet = useCallback((updater) => {
setRows((prev) => {
const next = typeof updater === 'function' ? updater(prev) : updater;
onChangeRef.current?.(serializeGroupTable(next));
return next;
});
}, []);
const updateRow = useCallback(
(id, field, value) => {
const next = rows.map((r) =>
r._id === id ? { ...r, [field]: value } : r,
emitAndSet((prev) =>
prev.map((r) => (r._id === id ? { ...r, [field]: value } : r)),
);
emitChange(next);
},
[rows, emitChange],
[emitAndSet],
);
const addRow = useCallback(() => {
const existingNames = new Set(rows.map((r) => r.name));
let counter = 1;
let newName = `group_${counter}`;
while (existingNames.has(newName)) {
counter++;
newName = `group_${counter}`;
}
emitChange([
...rows,
{
_id: uid(),
name: newName,
ratio: 1,
selectable: true,
description: '',
},
]);
}, [rows, emitChange]);
emitAndSet((prev) => {
const existingNames = new Set(prev.map((r) => r.name));
let counter = 1;
let newName = `group_${counter}`;
while (existingNames.has(newName)) {
counter++;
newName = `group_${counter}`;
}
return [
...prev,
{
_id: uid(),
name: newName,
ratio: 1,
selectable: true,
description: '',
},
];
});
}, [emitAndSet]);
const removeRow = useCallback(
(id) => {
emitChange(rows.filter((r) => r._id !== id));
emitAndSet((prev) => prev.filter((r) => r._id !== id));
},
[rows, emitChange],
[emitAndSet],
);
const groupNames = useMemo(() => rows.map((r) => r.name), [rows]);
@@ -127,6 +130,11 @@ export default function GroupTable({
return new Set(Object.keys(counts).filter((k) => counts[k] > 1));
}, [groupNames]);
// Use ref so column render functions always read the latest duplicate set
// without adding duplicateNames to columns deps (which would break cursor).
const duplicateNamesRef = useRef(duplicateNames);
duplicateNamesRef.current = duplicateNames;
const columns = useMemo(
() => [
{
@@ -138,7 +146,9 @@ export default function GroupTable({
<Input
size='small'
value={record.name}
status={duplicateNames.has(record.name) ? 'warning' : undefined}
status={
duplicateNamesRef.current.has(record.name) ? 'warning' : undefined
}
onChange={(v) => updateRow(record._id, 'name', v)}
/>
),
@@ -212,7 +222,7 @@ export default function GroupTable({
),
},
],
[t, duplicateNames, updateRow, removeRow],
[t, updateRow, removeRow],
);
return (
@@ -223,9 +233,7 @@ export default function GroupTable({
rowKey='_id'
hidePagination
size='small'
empty={
<Text type='tertiary'>{t('暂无分组,点击下方按钮添加')}</Text>
}
empty={<Text type='tertiary'>{t('暂无分组,点击下方按钮添加')}</Text>}
/>
<div className='mt-3 flex justify-center'>
<Button icon={<IconPlus />} theme='outline' onClick={addRow}>
@@ -234,7 +242,8 @@ export default function GroupTable({
</div>
{duplicateNames.size > 0 && (
<Text type='warning' size='small' className='mt-2 block'>
{t('存在重复的分组名称:')}{Array.from(duplicateNames).join(', ')}
{t('存在重复的分组名称:')}
{Array.from(duplicateNames).join(', ')}
</Text>
)}
</div>