- |
- toggleOne(token.id, e.target.checked)}
- />
- |
{token.name || '(unnamed)'} |
{tokenStatusLabel(token.status)}
|
- {!compact ? (
- <>
- {token.used_quota} |
- {formatUnixSeconds(token.created_time)} |
- >
- ) : null}
diff --git a/web-v2/src/pages/console/UsageLogsPage.tsx b/web-v2/src/pages/console/UsageLogsPage.tsx
index 9bd52db3e..032f0baf8 100644
--- a/web-v2/src/pages/console/UsageLogsPage.tsx
+++ b/web-v2/src/pages/console/UsageLogsPage.tsx
@@ -27,34 +27,6 @@ type LogRow = {
type PageInfo = { page: number; page_size: number; total: number; items: T };
-const DEFAULT_COLUMNS = [
- 'created_at',
- 'model_name',
- 'token_name',
- 'quota',
- 'prompt_tokens',
- 'completion_tokens',
- 'use_time',
-] as const;
-
-type ColumnKey = (typeof DEFAULT_COLUMNS)[number] | 'is_stream' | 'ip';
-
-function loadColumns(): ColumnKey[] {
- try {
- const raw = localStorage.getItem('logs-table-columns-user');
- if (!raw) return [...DEFAULT_COLUMNS];
- const parsed = JSON.parse(raw);
- if (!Array.isArray(parsed)) return [...DEFAULT_COLUMNS];
- return parsed as ColumnKey[];
- } catch {
- return [...DEFAULT_COLUMNS];
- }
-}
-
-function saveColumns(keys: ColumnKey[]) {
- localStorage.setItem('logs-table-columns-user', JSON.stringify(keys));
-}
-
function loadPageSize() {
const raw = localStorage.getItem('page-size');
const num = raw ? Number(raw) : 20;
@@ -87,7 +59,6 @@ export function UsageLogsPage() {
const [end, setEnd] = useState(() => toDateTimeLocalValueFromSeconds(Math.floor(Date.now() / 1000) + 3600));
const [stat, setStat] = useState<{ quota: number; rpm: number; tpm: number } | null>(null);
- const [columns, setColumns] = useState(() => loadColumns());
const [detail, setDetail] = useState(null);
@@ -137,14 +108,6 @@ export function UsageLogsPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [page, pageSize]);
- const toggleColumn = (key: ColumnKey, checked: boolean) => {
- setColumns((prev) => {
- const next = checked ? (prev.includes(key) ? prev : [...prev, key]) : prev.filter((c) => c !== key);
- saveColumns(next);
- return next;
- });
- };
-
return (
-
-
- Columns
- {([
- 'created_at',
- 'model_name',
- 'token_name',
- 'quota',
- 'prompt_tokens',
- 'completion_tokens',
- 'use_time',
- 'is_stream',
- 'ip',
- ] as ColumnKey[]).map((c) => (
-
- ))}
-
@@ -272,30 +211,30 @@ export function UsageLogsPage() {
- {columns.includes('created_at') ? | Time | : null}
- {columns.includes('model_name') ? Model | : null}
- {columns.includes('token_name') ? Token | : null}
- {columns.includes('quota') ? Cost | : null}
- {columns.includes('prompt_tokens') ? Prompt | : null}
- {columns.includes('completion_tokens') ? Completion | : null}
- {columns.includes('use_time') ? Time(s) | : null}
- {columns.includes('is_stream') ? Stream | : null}
- {columns.includes('ip') ? IP | : null}
+ Time |
+ Model |
+ Token |
+ Cost |
+ Prompt |
+ Completion |
+ Time(s) |
+ Stream |
+ IP |
Actions |
{items.map((row) => (
- {columns.includes('created_at') ? | {formatUnixSeconds(row.created_at)} | : null}
- {columns.includes('model_name') ? {row.model_name} | : null}
- {columns.includes('token_name') ? {row.token_name} | : null}
- {columns.includes('quota') ? ${(row.quota / quotaPerUnit).toFixed(6)} | : null}
- {columns.includes('prompt_tokens') ? {row.prompt_tokens} | : null}
- {columns.includes('completion_tokens') ? {row.completion_tokens} | : null}
- {columns.includes('use_time') ? {row.use_time} | : null}
- {columns.includes('is_stream') ? {row.is_stream ? 'Yes' : 'No'} | : null}
- {columns.includes('ip') ? {row.ip} | : null}
+ {formatUnixSeconds(row.created_at)} |
+ {row.model_name} |
+ {row.token_name} |
+ ${(row.quota / quotaPerUnit).toFixed(6)} |
+ {row.prompt_tokens} |
+ {row.completion_tokens} |
+ {row.use_time} |
+ {row.is_stream ? 'Yes' : 'No'} |
+ {row.ip} |
setDetail(row)}>
diff --git a/web/src/components/settings/OperationSetting.jsx b/web/src/components/settings/OperationSetting.jsx
index 4a77bcf10..16d4ffb17 100644
--- a/web/src/components/settings/OperationSetting.jsx
+++ b/web/src/components/settings/OperationSetting.jsx
@@ -27,6 +27,7 @@ import SettingsLog from '../../pages/Setting/Operation/SettingsLog';
import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring';
import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit';
import SettingsCheckin from '../../pages/Setting/Operation/SettingsCheckin';
+import SettingsLLMEndpoints from '../../pages/Setting/Operation/SettingsLLMEndpoints';
import { API, showError, toBoolean } from '../../helpers';
const OperationSetting = () => {
@@ -76,6 +77,19 @@ const OperationSetting = () => {
'checkin_setting.enabled': false,
'checkin_setting.min_quota': 1000,
'checkin_setting.max_quota': 10000,
+
+ /* LLM 端点开关 */
+ 'llm_endpoint_setting.enable_completions': true,
+ 'llm_endpoint_setting.enable_chat_completions': true,
+ 'llm_endpoint_setting.enable_responses': false,
+ 'llm_endpoint_setting.enable_claude_messages': false,
+ 'llm_endpoint_setting.enable_embeddings': false,
+ 'llm_endpoint_setting.enable_images': false,
+ 'llm_endpoint_setting.enable_audio': false,
+ 'llm_endpoint_setting.enable_moderations': false,
+ 'llm_endpoint_setting.enable_rerank': false,
+ 'llm_endpoint_setting.enable_realtime': false,
+ 'llm_endpoint_setting.enable_gemini': false,
});
let [loading, setLoading] = useState(false);
@@ -121,6 +135,10 @@ const OperationSetting = () => {
+ {/* LLM 端点开关 */}
+
+
+
{/* 顶栏模块管理 */}
diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx
index e8c4c5414..c903cd444 100644
--- a/web/src/components/table/channels/modals/EditChannelModal.jsx
+++ b/web/src/components/table/channels/modals/EditChannelModal.jsx
@@ -151,6 +151,7 @@ const EditChannelModal = (props) => {
multi_key_mode: 'random',
// 渠道额外设置的默认值
force_format: false,
+ completions_via_chat_completions: false,
thinking_to_content: false,
proxy: '',
pass_through_body_enabled: false,
@@ -370,10 +371,12 @@ const EditChannelModal = (props) => {
// 渠道额外设置状态
const [channelSettings, setChannelSettings] = useState({
force_format: false,
+ completions_via_chat_completions: false,
thinking_to_content: false,
proxy: '',
pass_through_body_enabled: false,
system_prompt: '',
+ system_prompt_override: false,
});
const showApiConfigCard = true; // 控制是否显示 API 配置卡片
const getInitValues = () => ({ ...originInputs });
@@ -576,6 +579,8 @@ const EditChannelModal = (props) => {
try {
const parsedSettings = JSON.parse(data.setting);
data.force_format = parsedSettings.force_format || false;
+ data.completions_via_chat_completions =
+ parsedSettings.completions_via_chat_completions || false;
data.thinking_to_content =
parsedSettings.thinking_to_content || false;
data.proxy = parsedSettings.proxy || '';
@@ -587,6 +592,7 @@ const EditChannelModal = (props) => {
} catch (error) {
console.error('Failed to parse channel settings JSON:', error);
data.force_format = false;
+ data.completions_via_chat_completions = false;
data.thinking_to_content = false;
data.proxy = '';
data.pass_through_body_enabled = false;
@@ -595,6 +601,7 @@ const EditChannelModal = (props) => {
}
} else {
data.force_format = false;
+ data.completions_via_chat_completions = false;
data.thinking_to_content = false;
data.proxy = '';
data.pass_through_body_enabled = false;
@@ -663,6 +670,7 @@ const EditChannelModal = (props) => {
// 同步更新channelSettings状态显示
setChannelSettings({
force_format: data.force_format,
+ completions_via_chat_completions: data.completions_via_chat_completions,
thinking_to_content: data.thinking_to_content,
proxy: data.proxy,
pass_through_body_enabled: data.pass_through_body_enabled,
@@ -1296,6 +1304,8 @@ const EditChannelModal = (props) => {
// 生成渠道额外设置JSON
const channelExtraSettings = {
force_format: localInputs.force_format || false,
+ completions_via_chat_completions:
+ localInputs.completions_via_chat_completions || false,
thinking_to_content: localInputs.thinking_to_content || false,
proxy: localInputs.proxy || '',
pass_through_body_enabled: localInputs.pass_through_body_enabled || false,
@@ -1347,6 +1357,7 @@ const EditChannelModal = (props) => {
// 清理不需要发送到后端的字段
delete localInputs.force_format;
+ delete localInputs.completions_via_chat_completions;
delete localInputs.thinking_to_content;
delete localInputs.proxy;
delete localInputs.pass_through_body_enabled;
@@ -3203,6 +3214,24 @@ const EditChannelModal = (props) => {
/>
)}
+ {inputs.type === 1 && (
+
+ handleChannelSettingsChange(
+ 'completions_via_chat_completions',
+ value,
+ )
+ }
+ extraText={t(
+ '开启后:/v1/completions 将通过 /v1/chat/completions 转发(最后一条消息使用 assistant role),并返回 completions 兼容格式',
+ )}
+ />
+ )}
+
.
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+import React, { useEffect, useRef, useState } from 'react';
+import { Banner, Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
+import { useTranslation } from 'react-i18next';
+import {
+ API,
+ compareObjects,
+ showError,
+ showSuccess,
+ showWarning,
+} from '../../../helpers';
+
+const defaultLLMEndpointInputs = {
+ 'llm_endpoint_setting.enable_completions': true,
+ 'llm_endpoint_setting.enable_chat_completions': true,
+ 'llm_endpoint_setting.enable_responses': false,
+ 'llm_endpoint_setting.enable_claude_messages': false,
+ 'llm_endpoint_setting.enable_embeddings': false,
+ 'llm_endpoint_setting.enable_images': false,
+ 'llm_endpoint_setting.enable_audio': false,
+ 'llm_endpoint_setting.enable_moderations': false,
+ 'llm_endpoint_setting.enable_rerank': false,
+ 'llm_endpoint_setting.enable_realtime': false,
+ 'llm_endpoint_setting.enable_gemini': false,
+};
+
+export default function SettingsLLMEndpoints(props) {
+ const { t } = useTranslation();
+ const [loading, setLoading] = useState(false);
+ const [inputs, setInputs] = useState(defaultLLMEndpointInputs);
+ const [inputsRow, setInputsRow] = useState(inputs);
+ const refForm = useRef();
+
+ function handleFieldChange(fieldName) {
+ return (value) => {
+ setInputs((prev) => ({ ...prev, [fieldName]: value }));
+ };
+ }
+
+ function onSubmit() {
+ const updateArray = compareObjects(inputs, inputsRow);
+ if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
+
+ const requestQueue = updateArray.map((item) =>
+ API.put('/api/option/', {
+ key: item.key,
+ value: String(inputs[item.key]),
+ }),
+ );
+
+ setLoading(true);
+ Promise.all(requestQueue)
+ .then((res) => {
+ if (requestQueue.length === 1) {
+ if (res.includes(undefined)) return;
+ } else if (requestQueue.length > 1) {
+ if (res.includes(undefined))
+ return showError(t('部分保存失败,请重试'));
+ }
+ showSuccess(t('保存成功'));
+ props.refresh();
+ })
+ .catch(() => {
+ showError(t('保存失败,请重试'));
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ }
+
+ useEffect(() => {
+ const currentInputs = {};
+ for (const key of Object.keys(defaultLLMEndpointInputs)) {
+ if (props.options[key] !== undefined) {
+ currentInputs[key] = props.options[key];
+ }
+ }
+
+ const mergedInputs = { ...defaultLLMEndpointInputs, ...currentInputs };
+ setInputs(mergedInputs);
+ setInputsRow(structuredClone(mergedInputs));
+ refForm.current?.setValues(mergedInputs);
+ }, [props.options]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
| |