feat: sync remaining channel and relay updates
This commit is contained in:
@@ -27,7 +27,10 @@ import {
|
||||
verifyJSON,
|
||||
} from '../../../../helpers';
|
||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||
import { CHANNEL_OPTIONS, MODEL_FETCHABLE_CHANNEL_TYPES } from '../../../../constants';
|
||||
import {
|
||||
CHANNEL_OPTIONS,
|
||||
MODEL_FETCHABLE_CHANNEL_TYPES,
|
||||
} from '../../../../constants';
|
||||
import {
|
||||
SideSheet,
|
||||
Space,
|
||||
@@ -128,7 +131,7 @@ const DEPRECATED_DOUBAO_CODING_PLAN_BASE_URL = 'doubao-coding-plan';
|
||||
|
||||
// 支持并且已适配通过接口获取模型列表的渠道类型
|
||||
const MODEL_FETCHABLE_TYPES = new Set([
|
||||
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43,
|
||||
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43, 58, 59, 60,
|
||||
]);
|
||||
|
||||
function type2secretPrompt(type) {
|
||||
@@ -277,7 +280,8 @@ const EditChannelModal = (props) => {
|
||||
[inputs.upstream_model_update_last_detected_models],
|
||||
);
|
||||
const upstreamDetectedModelsPreview = useMemo(
|
||||
() => upstreamDetectedModels.slice(0, UPSTREAM_DETECTED_MODEL_PREVIEW_LIMIT),
|
||||
() =>
|
||||
upstreamDetectedModels.slice(0, UPSTREAM_DETECTED_MODEL_PREVIEW_LIMIT),
|
||||
[upstreamDetectedModels],
|
||||
);
|
||||
const upstreamDetectedModelsOmittedCount =
|
||||
@@ -310,9 +314,7 @@ const EditChannelModal = (props) => {
|
||||
return {
|
||||
tagLabel: t('不更改'),
|
||||
tagColor: 'grey',
|
||||
preview: t(
|
||||
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数',
|
||||
),
|
||||
preview: t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数'),
|
||||
};
|
||||
}
|
||||
if (!verifyJSON(raw)) {
|
||||
@@ -1612,7 +1614,7 @@ const EditChannelModal = (props) => {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
localInputs.type === 45 &&
|
||||
[45, 58, 59, 60].includes(localInputs.type) &&
|
||||
(!localInputs.base_url || localInputs.base_url.trim() === '')
|
||||
) {
|
||||
showInfo(t('请输入API地址!'));
|
||||
@@ -2257,6 +2259,39 @@ const EditChannelModal = (props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 58 && (
|
||||
<Banner
|
||||
type='info'
|
||||
closeIcon={null}
|
||||
className='mb-4 rounded-xl'
|
||||
description={t(
|
||||
'该类型用于兼容上游把 OpenAI 官方兼容入口放在 /openai 的第三方站点。这里填写上游给官方 OpenAI 客户端或 Codex CLI 使用的 Base URL,例如:https://api.univibe.cc/openai;下游客户端仍然使用你自己的 New API 地址加 /v1。',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 59 && (
|
||||
<Banner
|
||||
type='info'
|
||||
closeIcon={null}
|
||||
className='mb-4 rounded-xl'
|
||||
description={t(
|
||||
'该类型用于兼容 Claude Code 或 Anthropic 网关类上游。这里填写上游提供的 Anthropic Base URL,例如:https://gateway.example.com/anthropic;转发时会保持 Anthropic 的 /v1/messages 路径,但按网关常见要求使用 Bearer 认证。',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 60 && (
|
||||
<Banner
|
||||
type='info'
|
||||
closeIcon={null}
|
||||
className='mb-4 rounded-xl'
|
||||
description={t(
|
||||
'该类型用于兼容 Gemini OpenAI 兼容入口。这里填写上游给 Gemini CLI 或 OpenAI 兼容 SDK 使用的 Base URL,例如:https://generativelanguage.googleapis.com/v1beta/openai;下游客户端仍然使用你自己的 New API 地址加 /v1。',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{inputs.type === 20 && (
|
||||
<Form.Switch
|
||||
field='is_enterprise_account'
|
||||
@@ -3001,6 +3036,9 @@ const EditChannelModal = (props) => {
|
||||
inputs.type !== 8 &&
|
||||
inputs.type !== 22 &&
|
||||
inputs.type !== 36 &&
|
||||
inputs.type !== 58 &&
|
||||
inputs.type !== 59 &&
|
||||
inputs.type !== 60 &&
|
||||
(inputs.type !== 45 || doubaoApiEditUnlocked) && (
|
||||
<div>
|
||||
<Form.Input
|
||||
@@ -3057,6 +3095,66 @@ const EditChannelModal = (props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inputs.type === 58 && (
|
||||
<div>
|
||||
<Form.Input
|
||||
field='base_url'
|
||||
label={t('上游 OpenAI / Codex Base URL')}
|
||||
placeholder={t(
|
||||
'请输入上游提供给官方 OpenAI 客户端或 Codex CLI 使用的 Base URL,例如:https://api.univibe.cc/openai',
|
||||
)}
|
||||
onChange={(value) =>
|
||||
handleInputChange('base_url', value)
|
||||
}
|
||||
showClear
|
||||
disabled={isIonetLocked}
|
||||
extraText={t(
|
||||
'此类型会把下游的 /v1/* 请求自动映射到上游的 /openai/*。这里不要填写 /v1,保持上游原始 Base URL 即可。',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inputs.type === 59 && (
|
||||
<div>
|
||||
<Form.Input
|
||||
field='base_url'
|
||||
label={t('上游 Claude Code / Anthropic Base URL')}
|
||||
placeholder={t(
|
||||
'请输入上游提供给 Claude Code 使用的 Anthropic Base URL,例如:https://gateway.example.com/anthropic',
|
||||
)}
|
||||
onChange={(value) =>
|
||||
handleInputChange('base_url', value)
|
||||
}
|
||||
showClear
|
||||
disabled={isIonetLocked}
|
||||
extraText={t(
|
||||
'此类型保持 Anthropic 官方请求路径,不自动补 /v1 到 Base URL;这里直接填写上游原始 Base URL 即可。',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inputs.type === 60 && (
|
||||
<div>
|
||||
<Form.Input
|
||||
field='base_url'
|
||||
label={t('上游 Gemini OpenAI Compat Base URL')}
|
||||
placeholder={t(
|
||||
'请输入 Gemini OpenAI 兼容 Base URL,例如:https://generativelanguage.googleapis.com/v1beta/openai',
|
||||
)}
|
||||
onChange={(value) =>
|
||||
handleInputChange('base_url', value)
|
||||
}
|
||||
showClear
|
||||
disabled={isIonetLocked}
|
||||
extraText={t(
|
||||
'此类型会把下游的 /v1/* 请求自动映射到上游的 /v1beta/openai/*。这里不要再额外填写 /v1。',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inputs.type === 45 && !doubaoApiEditUnlocked && (
|
||||
<div>
|
||||
<Form.Select
|
||||
@@ -3478,79 +3576,81 @@ const EditChannelModal = (props) => {
|
||||
/>
|
||||
|
||||
<Form.Switch
|
||||
field='upstream_model_update_auto_sync_enabled'
|
||||
label={t('是否自动同步上游模型更新')}
|
||||
checkedText={t('开')}
|
||||
uncheckedText={t('关')}
|
||||
disabled={!inputs.upstream_model_update_check_enabled}
|
||||
onChange={(value) =>
|
||||
handleChannelOtherSettingsChange(
|
||||
'upstream_model_update_auto_sync_enabled',
|
||||
value,
|
||||
)
|
||||
}
|
||||
extraText={t(
|
||||
'开启后检测到新增模型会自动加入当前渠道模型列表',
|
||||
)}
|
||||
field='upstream_model_update_auto_sync_enabled'
|
||||
label={t('是否自动同步上游模型更新')}
|
||||
checkedText={t('开')}
|
||||
uncheckedText={t('关')}
|
||||
disabled={!inputs.upstream_model_update_check_enabled}
|
||||
onChange={(value) =>
|
||||
handleChannelOtherSettingsChange(
|
||||
'upstream_model_update_auto_sync_enabled',
|
||||
value,
|
||||
)
|
||||
}
|
||||
extraText={t(
|
||||
'开启后检测到新增模型会自动加入当前渠道模型列表',
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className='text-xs text-gray-500 mb-3'>
|
||||
{t('上次检测到可加入模型')}:
|
||||
{upstreamDetectedModels.length === 0 ? (
|
||||
t('暂无')
|
||||
t('暂无')
|
||||
) : (
|
||||
<>
|
||||
<Tooltip
|
||||
position='topLeft'
|
||||
content={
|
||||
<div className='max-w-[640px] break-all text-xs leading-5'>
|
||||
{upstreamDetectedModels.join(', ')}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<>
|
||||
<Tooltip
|
||||
position='topLeft'
|
||||
content={
|
||||
<div className='max-w-[640px] break-all text-xs leading-5'>
|
||||
{upstreamDetectedModels.join(', ')}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span className='cursor-help break-all'>
|
||||
{upstreamDetectedModelsPreview.join(', ')}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<span className='ml-1 text-gray-400'>
|
||||
</Tooltip>
|
||||
<span className='ml-1 text-gray-400'>
|
||||
{upstreamDetectedModelsOmittedCount > 0
|
||||
? t('(共 {{total}} 个,省略 {{omit}} 个)', {
|
||||
? t('(共 {{total}} 个,省略 {{omit}} 个)', {
|
||||
total: upstreamDetectedModels.length,
|
||||
omit: upstreamDetectedModelsOmittedCount,
|
||||
})
|
||||
: t('(共 {{total}} 个)', {
|
||||
: t('(共 {{total}} 个)', {
|
||||
total: upstreamDetectedModels.length,
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='mb-4'>
|
||||
<div className='flex items-center justify-between gap-2 mb-1'>
|
||||
<Text className='text-sm font-medium'>{t('参数覆盖')}</Text>
|
||||
<Text className='text-sm font-medium'>
|
||||
{t('参数覆盖')}
|
||||
</Text>
|
||||
<Space wrap>
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
icon={<IconCode size={14} />}
|
||||
onClick={() => setParamOverrideEditorVisible(true)}
|
||||
size='small'
|
||||
type='primary'
|
||||
icon={<IconCode size={14} />}
|
||||
onClick={() => setParamOverrideEditorVisible(true)}
|
||||
>
|
||||
{t('可视化编辑')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
onClick={() =>
|
||||
applyParamOverrideTemplate('operations', 'fill')
|
||||
}
|
||||
size='small'
|
||||
onClick={() =>
|
||||
applyParamOverrideTemplate('operations', 'fill')
|
||||
}
|
||||
>
|
||||
{t('填充新模板')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
onClick={() =>
|
||||
applyParamOverrideTemplate('legacy', 'fill')
|
||||
}
|
||||
size='small'
|
||||
onClick={() =>
|
||||
applyParamOverrideTemplate('legacy', 'fill')
|
||||
}
|
||||
>
|
||||
{t('填充旧模板')}
|
||||
</Button>
|
||||
@@ -3564,14 +3664,16 @@ const EditChannelModal = (props) => {
|
||||
</Space>
|
||||
</div>
|
||||
<Text type='tertiary' size='small'>
|
||||
{t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')}
|
||||
{t(
|
||||
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数',
|
||||
)}
|
||||
</Text>
|
||||
<div
|
||||
className='mt-2 rounded-xl p-3'
|
||||
style={{
|
||||
backgroundColor: 'var(--semi-color-fill-0)',
|
||||
border: '1px solid var(--semi-color-fill-2)',
|
||||
}}
|
||||
className='mt-2 rounded-xl p-3'
|
||||
style={{
|
||||
backgroundColor: 'var(--semi-color-fill-0)',
|
||||
border: '1px solid var(--semi-color-fill-2)',
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center justify-between mb-2'>
|
||||
<Tag color={paramOverrideMeta.tagColor}>
|
||||
@@ -3579,17 +3681,19 @@ const EditChannelModal = (props) => {
|
||||
</Tag>
|
||||
<Space spacing={8}>
|
||||
<Button
|
||||
size='small'
|
||||
icon={<IconCopy />}
|
||||
type='tertiary'
|
||||
onClick={copyParamOverrideJson}
|
||||
size='small'
|
||||
icon={<IconCopy />}
|
||||
type='tertiary'
|
||||
onClick={copyParamOverrideJson}
|
||||
>
|
||||
{t('复制')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
onClick={() => setParamOverrideEditorVisible(true)}
|
||||
size='small'
|
||||
type='tertiary'
|
||||
onClick={() =>
|
||||
setParamOverrideEditorVisible(true)
|
||||
}
|
||||
>
|
||||
{t('编辑')}
|
||||
</Button>
|
||||
@@ -3602,80 +3706,80 @@ const EditChannelModal = (props) => {
|
||||
</div>
|
||||
|
||||
<Form.TextArea
|
||||
field='header_override'
|
||||
label={t('请求头覆盖')}
|
||||
placeholder={
|
||||
t('此项可选,用于覆盖请求头参数') +
|
||||
'\n' +
|
||||
t('格式示例:') +
|
||||
'\n{\n "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",\n "Authorization": "Bearer {api_key}"\n}'
|
||||
}
|
||||
autosize
|
||||
onChange={(value) =>
|
||||
handleInputChange('header_override', value)
|
||||
}
|
||||
extraText={
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex gap-2 flex-wrap items-center'>
|
||||
<Text
|
||||
className='!text-semi-color-primary cursor-pointer'
|
||||
onClick={() =>
|
||||
handleInputChange(
|
||||
'header_override',
|
||||
JSON.stringify(
|
||||
{
|
||||
'*': true,
|
||||
're:^X-Trace-.*$': true,
|
||||
'X-Foo': '{client_header:X-Foo}',
|
||||
Authorization: 'Bearer {api_key}',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('填入模板')}
|
||||
</Text>
|
||||
<Text
|
||||
className='!text-semi-color-primary cursor-pointer'
|
||||
onClick={() =>
|
||||
handleInputChange(
|
||||
'header_override',
|
||||
JSON.stringify(
|
||||
{
|
||||
'*': true,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('填入透传模版')}
|
||||
</Text>
|
||||
<Text
|
||||
className='!text-semi-color-primary cursor-pointer'
|
||||
onClick={() => formatJsonField('header_override')}
|
||||
>
|
||||
{t('格式化')}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type='tertiary' size='small'>
|
||||
{t('支持变量:')}
|
||||
</Text>
|
||||
<div className='text-xs text-tertiary ml-2'>
|
||||
<div>
|
||||
{t('渠道密钥')}: {'{api_key}'}
|
||||
</div>
|
||||
field='header_override'
|
||||
label={t('请求头覆盖')}
|
||||
placeholder={
|
||||
t('此项可选,用于覆盖请求头参数') +
|
||||
'\n' +
|
||||
t('格式示例:') +
|
||||
'\n{\n "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",\n "Authorization": "Bearer {api_key}"\n}'
|
||||
}
|
||||
autosize
|
||||
onChange={(value) =>
|
||||
handleInputChange('header_override', value)
|
||||
}
|
||||
extraText={
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex gap-2 flex-wrap items-center'>
|
||||
<Text
|
||||
className='!text-semi-color-primary cursor-pointer'
|
||||
onClick={() =>
|
||||
handleInputChange(
|
||||
'header_override',
|
||||
JSON.stringify(
|
||||
{
|
||||
'*': true,
|
||||
're:^X-Trace-.*$': true,
|
||||
'X-Foo': '{client_header:X-Foo}',
|
||||
Authorization: 'Bearer {api_key}',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0',
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('填入模板')}
|
||||
</Text>
|
||||
<Text
|
||||
className='!text-semi-color-primary cursor-pointer'
|
||||
onClick={() =>
|
||||
handleInputChange(
|
||||
'header_override',
|
||||
JSON.stringify(
|
||||
{
|
||||
'*': true,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t('填入透传模版')}
|
||||
</Text>
|
||||
<Text
|
||||
className='!text-semi-color-primary cursor-pointer'
|
||||
onClick={() => formatJsonField('header_override')}
|
||||
>
|
||||
{t('格式化')}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type='tertiary' size='small'>
|
||||
{t('支持变量:')}
|
||||
</Text>
|
||||
<div className='text-xs text-tertiary ml-2'>
|
||||
<div>
|
||||
{t('渠道密钥')}: {'{api_key}'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
showClear
|
||||
</div>
|
||||
}
|
||||
showClear
|
||||
/>
|
||||
<JSONEditor
|
||||
key={`status_code_mapping-${isEdit ? channelId : 'new'}`}
|
||||
|
||||
@@ -60,6 +60,7 @@ const ModelTestModal = ({
|
||||
const streamToggleDisabled = [
|
||||
'embeddings',
|
||||
'image-generation',
|
||||
'image-edit',
|
||||
'jina-rerank',
|
||||
'openai-response-compact',
|
||||
].includes(selectedEndpointType);
|
||||
@@ -96,6 +97,10 @@ const ModelTestModal = ({
|
||||
value: 'image-generation',
|
||||
label: t('图像生成') + ' (/v1/images/generations)',
|
||||
},
|
||||
{
|
||||
value: 'image-edit',
|
||||
label: 'OpenAI Image Edit (/v1/images/edits)',
|
||||
},
|
||||
{ value: 'embeddings', label: 'Embeddings (/v1/embeddings)' },
|
||||
];
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ const ENDPOINT_TEMPLATE = {
|
||||
gemini: { path: '/v1beta/models/{model}:generateContent', method: 'POST' },
|
||||
'jina-rerank': { path: '/v1/rerank', method: 'POST' },
|
||||
'image-generation': { path: '/v1/images/generations', method: 'POST' },
|
||||
'image-edit': { path: '/v1/images/edits', method: 'POST' },
|
||||
};
|
||||
|
||||
const nameRuleOptions = [
|
||||
|
||||
@@ -48,6 +48,7 @@ const ENDPOINT_TEMPLATE = {
|
||||
gemini: { path: '/v1beta/models/{model}:generateContent', method: 'POST' },
|
||||
'jina-rerank': { path: '/v1/rerank', method: 'POST' },
|
||||
'image-generation': { path: '/v1/images/generations', method: 'POST' },
|
||||
'image-edit': { path: '/v1/images/edits', method: 'POST' },
|
||||
};
|
||||
|
||||
const EditPrefillGroupModal = ({
|
||||
|
||||
17
web/src/constants/channel.constants.js
vendored
17
web/src/constants/channel.constants.js
vendored
@@ -189,11 +189,26 @@ export const CHANNEL_OPTIONS = [
|
||||
color: 'blue',
|
||||
label: 'Codex (OpenAI OAuth)',
|
||||
},
|
||||
{
|
||||
value: 58,
|
||||
color: 'cyan',
|
||||
label: 'OpenAI Official / Codex CLI (/openai BaseURL)',
|
||||
},
|
||||
{
|
||||
value: 59,
|
||||
color: 'indigo',
|
||||
label: 'Claude Code / Anthropic Gateway',
|
||||
},
|
||||
{
|
||||
value: 60,
|
||||
color: 'green',
|
||||
label: 'Gemini CLI / OpenAI Compat',
|
||||
},
|
||||
];
|
||||
|
||||
// Channel types that support upstream model list fetching in UI.
|
||||
export const MODEL_FETCHABLE_CHANNEL_TYPES = new Set([
|
||||
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43,
|
||||
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43, 58, 59, 60,
|
||||
]);
|
||||
|
||||
export const MODEL_TABLE_PAGE_SIZE = 10;
|
||||
|
||||
Reference in New Issue
Block a user