feat: support upstream model fetch for advanced custom channels (#5971)
* feat: support upstream model fetch for advanced custom channels * fix: add advanced custom routes as separate groups * fix: select advanced custom route entry before adding --------- Co-authored-by: CaIon <i@caion.me>
This commit is contained in:
+6
-2
@@ -523,12 +523,16 @@ export async function getTagModels(
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fetch models from a custom endpoint (for testing before creating channel)
|
||||
* Fetch models from the current unsaved channel form configuration.
|
||||
*/
|
||||
export async function fetchModels(data: {
|
||||
base_url: string
|
||||
type: number
|
||||
key: string
|
||||
key?: string
|
||||
channel_id?: number
|
||||
advanced_custom?: string
|
||||
header_override?: string
|
||||
proxy?: string
|
||||
}): Promise<FetchModelsResponse> {
|
||||
const res = await api.post(
|
||||
'/api/channel/fetch_models',
|
||||
|
||||
+155
-61
@@ -67,6 +67,7 @@ import {
|
||||
ADVANCED_CUSTOM_AUTH_MODE_OPTIONS,
|
||||
ADVANCED_CUSTOM_CONVERTER_OPTIONS,
|
||||
ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS,
|
||||
ADVANCED_CUSTOM_MODEL_LIST_PATH,
|
||||
ADVANCED_CUSTOM_TEMPLATE_OPTIONS,
|
||||
type AdvancedCustomAuthMode,
|
||||
buildAdvancedCustomAuth,
|
||||
@@ -215,6 +216,17 @@ export function AdvancedCustomEditorDialog({
|
||||
[routeKeys, routes]
|
||||
)
|
||||
const routeGroups = useMemo(() => buildRouteGroups(routeRows), [routeRows])
|
||||
const usedIncomingPaths = useMemo(
|
||||
() => new Set(routeGroups.map((routeGroup) => routeGroup.incomingPath)),
|
||||
[routeGroups]
|
||||
)
|
||||
const availableIncomingPathOptions = useMemo(
|
||||
() =>
|
||||
ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS.filter(
|
||||
(option) => !usedIncomingPaths.has(option.value)
|
||||
),
|
||||
[usedIncomingPaths]
|
||||
)
|
||||
const validationError = useMemo(
|
||||
() => validateAdvancedCustomConfig(normalizedConfig),
|
||||
[normalizedConfig]
|
||||
@@ -250,14 +262,19 @@ export function AdvancedCustomEditorDialog({
|
||||
setRouteKeys(nextRouteKeys)
|
||||
}
|
||||
|
||||
const addRoute = () => {
|
||||
const addRoute = (incomingPath: string | null) => {
|
||||
if (!incomingPath || usedIncomingPaths.has(incomingPath)) return
|
||||
setConfig((current) => {
|
||||
const next = normalizeAdvancedCustomConfig(current)
|
||||
return {
|
||||
...next,
|
||||
advanced_routes: [
|
||||
...(next.advanced_routes || []),
|
||||
createAdvancedCustomRoute(),
|
||||
{
|
||||
...createAdvancedCustomRoute(),
|
||||
incoming_path: incomingPath,
|
||||
upstream_path: incomingPath,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
@@ -308,6 +325,15 @@ export function AdvancedCustomEditorDialog({
|
||||
)
|
||||
const nextRoutes = routes.map((route, routeIndex) => {
|
||||
if (!groupRouteIndexes.has(routeIndex)) return route
|
||||
if (resolvedIncomingPath === ADVANCED_CUSTOM_MODEL_LIST_PATH) {
|
||||
return {
|
||||
...route,
|
||||
incoming_path: resolvedIncomingPath,
|
||||
upstream_path: ADVANCED_CUSTOM_MODEL_LIST_PATH,
|
||||
converter: 'none' as const,
|
||||
models: [],
|
||||
}
|
||||
}
|
||||
const converter = route.converter || 'none'
|
||||
return {
|
||||
...route,
|
||||
@@ -592,15 +618,45 @@ export function AdvancedCustomEditorDialog({
|
||||
{editMode === 'visual' ? (
|
||||
<div className='flex flex-col gap-4 p-4 lg:gap-3'>
|
||||
<div className='flex justify-end border-y py-4 lg:py-2'>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={addRoute}
|
||||
<Select
|
||||
items={availableIncomingPathOptions.map((option) => option.value)}
|
||||
value={null}
|
||||
onValueChange={(incomingPath) => {
|
||||
if (typeof incomingPath === 'string') {
|
||||
addRoute(incomingPath)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus data-icon='inline-start' />
|
||||
{t('Add route')}
|
||||
</Button>
|
||||
<SelectTrigger
|
||||
size='sm'
|
||||
disabled={availableIncomingPathOptions.length === 0}
|
||||
>
|
||||
<Plus data-icon='inline-start' />
|
||||
<SelectValue placeholder={t('Add route')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
align='end'
|
||||
alignItemWithTrigger={false}
|
||||
className={longSelectContentClass}
|
||||
>
|
||||
<SelectGroup>
|
||||
{availableIncomingPathOptions.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={longSelectItemClass}
|
||||
>
|
||||
<div className='flex min-w-0 flex-col gap-1 leading-snug whitespace-normal'>
|
||||
<span>{t(option.label)}</span>
|
||||
<span className='text-muted-foreground font-mono text-xs break-all'>
|
||||
{option.value}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{validationError ? (
|
||||
@@ -635,6 +691,7 @@ export function AdvancedCustomEditorDialog({
|
||||
<RouteGroupEditor
|
||||
key={routeGroup.incomingPath || 'advanced-custom-empty-path'}
|
||||
group={routeGroup}
|
||||
usedIncomingPaths={usedIncomingPaths}
|
||||
validationError={validationError}
|
||||
onAddRoute={() =>
|
||||
addRouteForIncomingPath(routeGroup.incomingPath)
|
||||
@@ -690,6 +747,7 @@ export function AdvancedCustomEditorDialog({
|
||||
|
||||
function RouteGroupEditor({
|
||||
group,
|
||||
usedIncomingPaths,
|
||||
validationError,
|
||||
onAddRoute,
|
||||
onIncomingPathChange,
|
||||
@@ -699,6 +757,7 @@ function RouteGroupEditor({
|
||||
onRouteChange,
|
||||
}: {
|
||||
group: AdvancedCustomRouteGroup
|
||||
usedIncomingPaths: ReadonlySet<string>
|
||||
validationError: ReturnType<typeof validateAdvancedCustomConfig>
|
||||
onAddRoute: () => void
|
||||
onIncomingPathChange: (incomingPath: string | null) => void
|
||||
@@ -709,6 +768,7 @@ function RouteGroupEditor({
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const incomingPath = group.incomingPath || '/v1/chat/completions'
|
||||
const isModelListGroup = incomingPath === ADVANCED_CUSTOM_MODEL_LIST_PATH
|
||||
const incomingPathLabel = getAdvancedCustomIncomingPathLabel(incomingPath)
|
||||
const catchAllRoute = group.routeRows.find((routeRow) =>
|
||||
isCatchAllRoute(routeRow.route)
|
||||
@@ -741,17 +801,21 @@ function RouteGroupEditor({
|
||||
<Badge variant='secondary'>
|
||||
{group.routeRows.length} {t('Routes')}
|
||||
</Badge>
|
||||
<Badge variant={hasCatchAll ? 'outline' : 'secondary'}>
|
||||
{hasCatchAll ? t('Fallback route') : t('Model-scoped only')}
|
||||
</Badge>
|
||||
{!catchAllIsLast ? (
|
||||
{isModelListGroup ? (
|
||||
<Badge variant='outline'>{t('OpenAI Models')}</Badge>
|
||||
) : (
|
||||
<Badge variant={hasCatchAll ? 'outline' : 'secondary'}>
|
||||
{hasCatchAll ? t('Fallback route') : t('Model-scoped only')}
|
||||
</Badge>
|
||||
)}
|
||||
{!isModelListGroup && !catchAllIsLast ? (
|
||||
<Badge variant='destructive'>{t('Fallback must be last')}</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<Select value={incomingPath} onValueChange={onIncomingPathChange}>
|
||||
<SelectTrigger className='h-9 max-w-full lg:max-w-[420px]'>
|
||||
<SelectValue className='min-w-0 truncate'>
|
||||
{incomingPathLabel}
|
||||
{t(incomingPathLabel)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
@@ -763,10 +827,16 @@ function RouteGroupEditor({
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
disabled={
|
||||
(option.value !== incomingPath &&
|
||||
usedIncomingPaths.has(option.value)) ||
|
||||
(option.value === ADVANCED_CUSTOM_MODEL_LIST_PATH &&
|
||||
group.routeRows.length > 1)
|
||||
}
|
||||
className={longSelectItemClass}
|
||||
>
|
||||
<div className='flex min-w-0 flex-col gap-1 leading-snug whitespace-normal'>
|
||||
<span>{option.label}</span>
|
||||
<span>{t(option.label)}</span>
|
||||
<span className='text-muted-foreground font-mono text-xs break-all'>
|
||||
{option.value}
|
||||
</span>
|
||||
@@ -778,17 +848,28 @@ function RouteGroupEditor({
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button type='button' variant='outline' size='sm' onClick={onAddRoute}>
|
||||
<Plus data-icon='inline-start' />
|
||||
{t('Add split')}
|
||||
</Button>
|
||||
{!isModelListGroup ? (
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={onAddRoute}
|
||||
>
|
||||
<Plus data-icon='inline-start' />
|
||||
{t('Add split')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className='border-t px-3 py-2'>
|
||||
<p className='text-muted-foreground text-xs leading-relaxed'>
|
||||
{t(
|
||||
'Routes with the same incoming path are split by client model rules. Unmatched requests use the final fallback.'
|
||||
)}
|
||||
{isModelListGroup
|
||||
? t(
|
||||
'This route discovers upstream OpenAI models and cannot be split or matched by client model rules.'
|
||||
)
|
||||
: t(
|
||||
'Routes with the same incoming path are split by client model rules. Unmatched requests use the final fallback.'
|
||||
)}
|
||||
</p>
|
||||
{groupHasError && validationError ? (
|
||||
<p className='text-destructive mt-1 text-xs'>
|
||||
@@ -880,6 +961,7 @@ function RouteEditor({
|
||||
const authMode = getAdvancedCustomAuthMode(route)
|
||||
const incomingPath =
|
||||
route.incoming_path || getDefaultAdvancedCustomIncomingPath(converter)
|
||||
const isModelListRoute = incomingPath === ADVANCED_CUSTOM_MODEL_LIST_PATH
|
||||
const converterOptions = useMemo(
|
||||
() => getAdvancedCustomConverterOptions(incomingPath),
|
||||
[incomingPath]
|
||||
@@ -897,7 +979,7 @@ function RouteEditor({
|
||||
const ConverterVisualIcon = isNativeConverter ? ArrowRight : Shuffle
|
||||
const modelsInputValue = route.models?.join(', ') || ''
|
||||
const parsedRouteModels = parseAdvancedCustomRouteModels(modelsInputValue)
|
||||
const isFallback = parsedRouteModels.length === 0
|
||||
const isFallback = !isModelListRoute && parsedRouteModels.length === 0
|
||||
|
||||
const setConverter = (nextConverter: AdvancedCustomConverter) => {
|
||||
let nextIncomingPath = incomingPath
|
||||
@@ -965,7 +1047,10 @@ function RouteEditor({
|
||||
<div className='text-sm font-medium'>
|
||||
{t('Route')} {index + 1}
|
||||
</div>
|
||||
{isFallback ? (
|
||||
{isModelListRoute ? (
|
||||
<Badge variant='outline'>{t('OpenAI Models')}</Badge>
|
||||
) : null}
|
||||
{!isModelListRoute && isFallback ? (
|
||||
<Badge variant='outline'>{t('Fallback')}</Badge>
|
||||
) : null}
|
||||
<TooltipProvider delay={100}>
|
||||
@@ -1033,42 +1118,50 @@ function RouteEditor({
|
||||
className='lg:gap-1'
|
||||
labelClassName='lg:sr-only'
|
||||
>
|
||||
<Input
|
||||
value={modelsInputValue}
|
||||
onChange={(event) => setModelsInput(event.target.value)}
|
||||
onBlur={(event) => normalizeModelsInput(event.target.value)}
|
||||
placeholder={
|
||||
isFallback
|
||||
? t('Leave empty for fallback')
|
||||
: t('e.g. gpt-4o, gemini-2.5-flash')
|
||||
}
|
||||
aria-invalid={Boolean(errorMessage)}
|
||||
/>
|
||||
<div className='flex flex-wrap gap-1'>
|
||||
{isFallback ? (
|
||||
<Badge variant='outline'>{t('Fallback')}</Badge>
|
||||
) : (
|
||||
parsedRouteModels.map((model) => {
|
||||
const ruleKind = getAdvancedCustomModelRuleKind(model)
|
||||
const displayModel =
|
||||
ruleKind === 'regex'
|
||||
? getAdvancedCustomRegexModelPattern(model) || model
|
||||
: model
|
||||
return (
|
||||
<Badge
|
||||
key={model}
|
||||
variant={ruleKind === 'regex' ? 'outline' : 'secondary'}
|
||||
className='max-w-full gap-1.5 font-mono'
|
||||
>
|
||||
<span className='font-sans text-[10px] font-semibold tracking-normal uppercase'>
|
||||
{t(ruleKind === 'regex' ? 'Regex' : 'Exact')}
|
||||
</span>
|
||||
<span className='truncate'>{displayModel}</span>
|
||||
</Badge>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
{isModelListRoute && parsedRouteModels.length === 0 ? (
|
||||
<div className='flex h-9 items-center'>
|
||||
<Badge variant='outline'>{t('OpenAI Models')}</Badge>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
value={modelsInputValue}
|
||||
onChange={(event) => setModelsInput(event.target.value)}
|
||||
onBlur={(event) => normalizeModelsInput(event.target.value)}
|
||||
placeholder={
|
||||
isFallback
|
||||
? t('Leave empty for fallback')
|
||||
: t('e.g. gpt-4o, gemini-2.5-flash')
|
||||
}
|
||||
aria-invalid={Boolean(errorMessage)}
|
||||
/>
|
||||
<div className='flex flex-wrap gap-1'>
|
||||
{isFallback ? (
|
||||
<Badge variant='outline'>{t('Fallback')}</Badge>
|
||||
) : (
|
||||
parsedRouteModels.map((model) => {
|
||||
const ruleKind = getAdvancedCustomModelRuleKind(model)
|
||||
const displayModel =
|
||||
ruleKind === 'regex'
|
||||
? getAdvancedCustomRegexModelPattern(model) || model
|
||||
: model
|
||||
return (
|
||||
<Badge
|
||||
key={model}
|
||||
variant={ruleKind === 'regex' ? 'outline' : 'secondary'}
|
||||
className='max-w-full gap-1.5 font-mono'
|
||||
>
|
||||
<span className='font-sans text-[10px] font-semibold tracking-normal uppercase'>
|
||||
{t(ruleKind === 'regex' ? 'Regex' : 'Exact')}
|
||||
</span>
|
||||
<span className='truncate'>{displayModel}</span>
|
||||
</Badge>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</FieldBlock>
|
||||
|
||||
<FieldBlock
|
||||
@@ -1100,6 +1193,7 @@ function RouteEditor({
|
||||
>
|
||||
<Select
|
||||
value={converter}
|
||||
disabled={isModelListRoute && converter === 'none'}
|
||||
onValueChange={(value) =>
|
||||
setConverter(value as AdvancedCustomConverter)
|
||||
}
|
||||
|
||||
+31
-13
@@ -759,6 +759,9 @@ export function ChannelMutateDrawer({
|
||||
const currentUpstreamModelUpdateIgnoredModels = form.watch(
|
||||
'upstream_model_update_ignored_models'
|
||||
)
|
||||
const shouldPreviewUnsavedModels =
|
||||
!isEditing ||
|
||||
(currentType === CHANNEL_TYPE_ADVANCED_CUSTOM && canEditSensitive)
|
||||
const {
|
||||
unlocked: doubaoApiEditUnlocked,
|
||||
handleClick: handleApiConfigSecretClick,
|
||||
@@ -866,7 +869,7 @@ export function ChannelMutateDrawer({
|
||||
advancedCustomRouteTypeLabels.length
|
||||
const advancedCustomRouteTypeTitle =
|
||||
hiddenAdvancedCustomRouteTypeCount > 0
|
||||
? advancedCustomStats.routeTypeLabels.join(', ')
|
||||
? advancedCustomStats.routeTypeLabels.map((label) => t(label)).join(', ')
|
||||
: undefined
|
||||
|
||||
// Get all models list
|
||||
@@ -1421,8 +1424,8 @@ export function ChannelMutateDrawer({
|
||||
return
|
||||
}
|
||||
|
||||
// For creation mode, validate key before opening dialog
|
||||
if (!isEditing) {
|
||||
// Advanced Custom may use a model discovery route with no authentication.
|
||||
if (!isEditing && type !== CHANNEL_TYPE_ADVANCED_CUSTOM) {
|
||||
const key = form.getValues('key')
|
||||
if (!key?.trim()) {
|
||||
toast.error(t('Please enter API key first'))
|
||||
@@ -1433,20 +1436,30 @@ export function ChannelMutateDrawer({
|
||||
setFetchModelsDialogOpen(true)
|
||||
}, [isEditing, canEditSensitive, form, t])
|
||||
|
||||
const createModeFetcher = useCallback(async (): Promise<string[]> => {
|
||||
const formPreviewFetcher = useCallback(async (): Promise<string[]> => {
|
||||
if (!canEditSensitive) {
|
||||
throw new Error(t("You don't have necessary permission"))
|
||||
}
|
||||
const type = form.getValues('type')
|
||||
const editingAdvancedCustom =
|
||||
isEditing && type === CHANNEL_TYPE_ADVANCED_CUSTOM
|
||||
if (editingAdvancedCustom && channelId === null) {
|
||||
throw new Error(t('No channel selected'))
|
||||
}
|
||||
const response = await fetchModels({
|
||||
type: form.getValues('type'),
|
||||
key: form.getValues('key'),
|
||||
type,
|
||||
key: isEditing ? undefined : form.getValues('key'),
|
||||
channel_id: editingAdvancedCustom ? channelId || undefined : undefined,
|
||||
base_url: form.getValues('base_url') || '',
|
||||
advanced_custom: form.getValues('advanced_custom'),
|
||||
header_override: form.getValues('header_override'),
|
||||
proxy: form.getValues('proxy'),
|
||||
})
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
}
|
||||
throw new Error(response.message || 'No models fetched from upstream')
|
||||
}, [canEditSensitive, form, t])
|
||||
throw new Error(response.message || t('No models fetched from upstream'))
|
||||
}, [canEditSensitive, channelId, form, isEditing, t])
|
||||
|
||||
// Handle model operations
|
||||
const handleFillRelatedModels = useCallback(() => {
|
||||
@@ -2787,10 +2800,10 @@ export function ChannelMutateDrawer({
|
||||
key={label}
|
||||
variant='outline'
|
||||
className='max-w-[12rem]'
|
||||
title={label}
|
||||
title={t(label)}
|
||||
>
|
||||
<span className='truncate'>
|
||||
{label}
|
||||
{t(label)}
|
||||
</span>
|
||||
</Badge>
|
||||
)
|
||||
@@ -4514,6 +4527,7 @@ export function ChannelMutateDrawer({
|
||||
'Periodically check for upstream model changes'
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
@@ -4682,10 +4696,14 @@ export function ChannelMutateDrawer({
|
||||
}}
|
||||
redirectModels={redirectModelList}
|
||||
redirectSourceModels={redirectModelKeyList}
|
||||
customFetcher={!isEditing ? createModeFetcher : undefined}
|
||||
channelName={!isEditing ? currentName?.trim() : undefined}
|
||||
customFetcher={
|
||||
shouldPreviewUnsavedModels ? formPreviewFetcher : undefined
|
||||
}
|
||||
channelName={
|
||||
shouldPreviewUnsavedModels ? currentName?.trim() : undefined
|
||||
}
|
||||
existingModelsOverride={
|
||||
!isEditing
|
||||
shouldPreviewUnsavedModels
|
||||
? parseModelsString(form.getValues('models') || '')
|
||||
: undefined
|
||||
}
|
||||
|
||||
+1
-1
@@ -377,7 +377,7 @@ export const FIELD_DESCRIPTIONS = {
|
||||
// ============================================================================
|
||||
|
||||
export const MODEL_FETCHABLE_TYPES = new Set([
|
||||
1, 4, 14, 17, 20, 23, 24, 25, 26, 27, 31, 34, 35, 40, 42, 43, 47, 48, 57,
|
||||
1, 4, 14, 17, 20, 23, 24, 25, 26, 27, 31, 34, 35, 40, 42, 43, 47, 48, 57, 58,
|
||||
])
|
||||
|
||||
export const TYPE_TO_KEY_PROMPT: Record<number, string> = {
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
} from '../types'
|
||||
|
||||
export const CHANNEL_TYPE_ADVANCED_CUSTOM = 58
|
||||
export const ADVANCED_CUSTOM_MODEL_LIST_PATH = '/v1/models'
|
||||
|
||||
export const ADVANCED_CUSTOM_CONVERTER_OPTIONS: Array<{
|
||||
value: AdvancedCustomConverter
|
||||
@@ -104,6 +105,10 @@ export const ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS: AdvancedCustomIncomingPathOp
|
||||
value: '/v1/responses/compact',
|
||||
label: 'OpenAI Responses Compact',
|
||||
},
|
||||
{
|
||||
value: ADVANCED_CUSTOM_MODEL_LIST_PATH,
|
||||
label: 'OpenAI Models',
|
||||
},
|
||||
{
|
||||
value: '/v1/embeddings',
|
||||
label: 'OpenAI Embeddings',
|
||||
@@ -160,6 +165,7 @@ export const ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS: AdvancedCustomIncomingPathOp
|
||||
|
||||
const ADVANCED_CUSTOM_ROUTE_SUMMARY_LABELS: Record<string, string> = {
|
||||
'/v1/chat/completions': 'OpenAI Chat',
|
||||
[ADVANCED_CUSTOM_MODEL_LIST_PATH]: 'OpenAI Models',
|
||||
}
|
||||
|
||||
export type AdvancedCustomValidationError = {
|
||||
@@ -537,6 +543,7 @@ export function validateAdvancedCustomConfig(
|
||||
string,
|
||||
{ catchAllIndex: number | null; models: Map<string, number> }
|
||||
>()
|
||||
let modelListRouteIndex: number | null = null
|
||||
for (let index = 0; index < routes.length; index += 1) {
|
||||
const route = routes[index]
|
||||
const incomingPath = route.incoming_path?.trim() || ''
|
||||
@@ -556,6 +563,33 @@ export function validateAdvancedCustomConfig(
|
||||
message: 'Incoming path must not include query',
|
||||
}
|
||||
}
|
||||
if (incomingPath === ADVANCED_CUSTOM_MODEL_LIST_PATH) {
|
||||
if (modelListRouteIndex !== null) {
|
||||
return {
|
||||
routeIndex: index,
|
||||
message: 'Only one OpenAI Models route is allowed',
|
||||
}
|
||||
}
|
||||
modelListRouteIndex = index
|
||||
if (routeModels.length > 0) {
|
||||
return {
|
||||
routeIndex: index,
|
||||
message: 'OpenAI Models route does not support client model rules',
|
||||
}
|
||||
}
|
||||
if (converter !== 'none') {
|
||||
return {
|
||||
routeIndex: index,
|
||||
message: 'OpenAI Models route must use native forwarding',
|
||||
}
|
||||
}
|
||||
if (upstreamPath.includes('{model}')) {
|
||||
return {
|
||||
routeIndex: index,
|
||||
message: 'OpenAI Models upstream path must not contain {model}',
|
||||
}
|
||||
}
|
||||
}
|
||||
const routeModelsError = validateAdvancedCustomRouteModels(
|
||||
index,
|
||||
incomingPath,
|
||||
@@ -594,6 +628,16 @@ export function validateAdvancedCustomConfig(
|
||||
return null
|
||||
}
|
||||
|
||||
export function hasValidAdvancedCustomModelListRoute(
|
||||
config: AdvancedCustomConfig | null
|
||||
): boolean {
|
||||
if (!config || validateAdvancedCustomConfig(config)) return false
|
||||
const normalized = normalizeAdvancedCustomConfig(config)
|
||||
return (normalized.advanced_routes || []).some(
|
||||
(route) => route.incoming_path?.trim() === ADVANCED_CUSTOM_MODEL_LIST_PATH
|
||||
)
|
||||
}
|
||||
|
||||
export function advancedCustomConfigUsesRelativeUpstreamPath(
|
||||
config: AdvancedCustomConfig | null
|
||||
): boolean {
|
||||
|
||||
+30
-10
@@ -27,6 +27,7 @@ import type { Channel } from '../types'
|
||||
import {
|
||||
CHANNEL_TYPE_ADVANCED_CUSTOM,
|
||||
advancedCustomConfigUsesRelativeUpstreamPath,
|
||||
hasValidAdvancedCustomModelListRoute,
|
||||
parseAdvancedCustomConfig,
|
||||
stringifyAdvancedCustomConfig,
|
||||
validateAdvancedCustomConfig,
|
||||
@@ -238,6 +239,16 @@ export const channelFormSchema = z
|
||||
'Base URL is required when an advanced route uses an upstream path'
|
||||
)
|
||||
}
|
||||
if (
|
||||
data.upstream_model_update_check_enabled === true &&
|
||||
!hasValidAdvancedCustomModelListRoute(advancedCustomConfig)
|
||||
) {
|
||||
addRequiredIssue(
|
||||
ctx,
|
||||
'upstream_model_update_check_enabled',
|
||||
'OpenAI Models route is required to enable upstream model checks'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if ([3, 18, 21, 39, 41, 49].includes(data.type) && !data.other?.trim()) {
|
||||
@@ -563,13 +574,18 @@ function buildSettingsJSON(formData: ChannelFormValues): string {
|
||||
formData.allow_include_obfuscation === true
|
||||
settingsObj.allow_inference_geo = formData.allow_inference_geo === true
|
||||
} else {
|
||||
if ('disable_store' in settingsObj) delete settingsObj.disable_store
|
||||
if ('allow_safety_identifier' in settingsObj)
|
||||
if ('disable_store' in settingsObj) {
|
||||
delete settingsObj.disable_store
|
||||
}
|
||||
if ('allow_safety_identifier' in settingsObj) {
|
||||
delete settingsObj.allow_safety_identifier
|
||||
if ('allow_include_obfuscation' in settingsObj)
|
||||
}
|
||||
if ('allow_include_obfuscation' in settingsObj) {
|
||||
delete settingsObj.allow_include_obfuscation
|
||||
if (formData.type !== 14 && 'allow_inference_geo' in settingsObj)
|
||||
}
|
||||
if (formData.type !== 14 && 'allow_inference_geo' in settingsObj) {
|
||||
delete settingsObj.allow_inference_geo
|
||||
}
|
||||
}
|
||||
|
||||
// Anthropic (type 14): claude_beta_query, allow_inference_geo, allow_speed
|
||||
@@ -578,8 +594,12 @@ function buildSettingsJSON(formData: ChannelFormValues): string {
|
||||
settingsObj.allow_speed = formData.allow_speed === true
|
||||
settingsObj.claude_beta_query = formData.claude_beta_query === true
|
||||
} else {
|
||||
if ('allow_speed' in settingsObj) delete settingsObj.allow_speed
|
||||
if ('claude_beta_query' in settingsObj) delete settingsObj.claude_beta_query
|
||||
if ('allow_speed' in settingsObj) {
|
||||
delete settingsObj.allow_speed
|
||||
}
|
||||
if ('claude_beta_query' in settingsObj) {
|
||||
delete settingsObj.claude_beta_query
|
||||
}
|
||||
}
|
||||
|
||||
settingsObj.disable_task_polling_sleep =
|
||||
@@ -592,14 +612,14 @@ function buildSettingsJSON(formData: ChannelFormValues): string {
|
||||
settingsObj.upstream_model_update_auto_sync_enabled =
|
||||
settingsObj.upstream_model_update_check_enabled === true &&
|
||||
formData.upstream_model_update_auto_sync_enabled === true
|
||||
settingsObj.upstream_model_update_ignored_models = Array.from(
|
||||
new Set(
|
||||
settingsObj.upstream_model_update_ignored_models = [
|
||||
...new Set(
|
||||
String(formData.upstream_model_update_ignored_models || '')
|
||||
.split(',')
|
||||
.map((model) => model.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
)
|
||||
),
|
||||
]
|
||||
if (
|
||||
!Array.isArray(settingsObj.upstream_model_update_last_detected_models) ||
|
||||
settingsObj.upstream_model_update_check_enabled !== true
|
||||
|
||||
Vendored
+7
@@ -3084,6 +3084,7 @@
|
||||
"Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.": "Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.",
|
||||
"Only Mine": "Only Mine",
|
||||
"Only one catch-all route is allowed for the same incoming path": "Only one catch-all route is allowed for the same incoming path",
|
||||
"Only one OpenAI Models route is allowed": "Only one OpenAI Models route is allowed",
|
||||
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.",
|
||||
"Only successful requests": "Only successful requests",
|
||||
"Only successful requests count toward this limit.": "Only successful requests count toward this limit.",
|
||||
@@ -3116,6 +3117,11 @@
|
||||
"OpenAI Embeddings": "OpenAI Embeddings",
|
||||
"OpenAI Image Edits": "OpenAI Image Edits",
|
||||
"OpenAI Image Generations": "OpenAI Image Generations",
|
||||
"OpenAI Models": "OpenAI Models",
|
||||
"OpenAI Models route does not support client model rules": "OpenAI Models route does not support client model rules",
|
||||
"OpenAI Models route is required to enable upstream model checks": "OpenAI Models route is required to enable upstream model checks",
|
||||
"OpenAI Models route must use native forwarding": "OpenAI Models route must use native forwarding",
|
||||
"OpenAI Models upstream path must not contain {model}": "OpenAI Models upstream path must not contain {model}",
|
||||
"OpenAI Organization": "OpenAI Organization",
|
||||
"OpenAI Organization ID (optional)": "OpenAI Organization ID (optional)",
|
||||
"OpenAI Realtime": "OpenAI Realtime",
|
||||
@@ -4528,6 +4534,7 @@
|
||||
"This plan does not allow balance redemption": "This plan does not allow balance redemption",
|
||||
"This project must be used in compliance with the": "This project must be used in compliance with the",
|
||||
"This removes {{count}} failed models from this channel. This action cannot be undone.": "This removes {{count}} failed models from this channel. This action cannot be undone.",
|
||||
"This route discovers upstream OpenAI models and cannot be split or matched by client model rules.": "This route discovers upstream OpenAI models and cannot be split or matched by client model rules.",
|
||||
"This site currently has {{count}} models enabled": "This site currently has {{count}} models enabled",
|
||||
"This tier catches any request that did not match earlier tiers.": "This tier catches any request that did not match earlier tiers.",
|
||||
"this token group": "this token group",
|
||||
|
||||
Vendored
+7
@@ -3084,6 +3084,7 @@
|
||||
"Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.": "Saisissez uniquement l’origine du site, par exemple https://api.example.com. N’ajoutez aucun chemin comme /api/user/epay/notify. Laissez vide pour utiliser l’adresse du serveur.",
|
||||
"Only Mine": "Uniquement les miens",
|
||||
"Only one catch-all route is allowed for the same incoming path": "Un seul routage de secours est autorisé pour le même chemin d'entrée",
|
||||
"Only one OpenAI Models route is allowed": "Une seule route Modèles OpenAI est autorisée",
|
||||
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Seuls les champs sélectionnés seront écrasés. Vous pouvez relancer l'assistant de synchronisation si de nouveaux conflits apparaissent.",
|
||||
"Only successful requests": "Uniquement les requêtes réussies",
|
||||
"Only successful requests count toward this limit.": "Seules les requêtes réussies comptent pour cette limite.",
|
||||
@@ -3116,6 +3117,11 @@
|
||||
"OpenAI Embeddings": "OpenAI Embeddings",
|
||||
"OpenAI Image Edits": "OpenAI Image Edits",
|
||||
"OpenAI Image Generations": "OpenAI Image Generations",
|
||||
"OpenAI Models": "Modèles OpenAI",
|
||||
"OpenAI Models route does not support client model rules": "La route Modèles OpenAI ne prend pas en charge les règles de modèles clients",
|
||||
"OpenAI Models route is required to enable upstream model checks": "La route Modèles OpenAI est requise pour activer la vérification des modèles en amont",
|
||||
"OpenAI Models route must use native forwarding": "La route Modèles OpenAI doit utiliser le transfert natif",
|
||||
"OpenAI Models upstream path must not contain {model}": "Le chemin amont de la route Modèles OpenAI ne doit pas contenir {model}",
|
||||
"OpenAI Organization": "Organisation OpenAI",
|
||||
"OpenAI Organization ID (optional)": "Identifiant d'organisation OpenAI (optionnel)",
|
||||
"OpenAI Realtime": "OpenAI Realtime",
|
||||
@@ -4528,6 +4534,7 @@
|
||||
"This plan does not allow balance redemption": "Ce forfait ne permet pas le paiement avec le solde",
|
||||
"This project must be used in compliance with the": "Ce projet doit être utilisé conformément aux",
|
||||
"This removes {{count}} failed models from this channel. This action cannot be undone.": "Cela supprime {{count}} modèles en échec de ce canal. Cette action est irréversible.",
|
||||
"This route discovers upstream OpenAI models and cannot be split or matched by client model rules.": "Cette route découvre les modèles OpenAI en amont et ne peut être ni divisée ni associée par des règles de modèles clients.",
|
||||
"This site currently has {{count}} models enabled": "Ce site compte actuellement {{count}} modèles activés",
|
||||
"This tier catches any request that did not match earlier tiers.": "Ce palier récupère toute requête qui ne correspond à aucun palier précédent.",
|
||||
"this token group": "ce groupe de jetons",
|
||||
|
||||
Vendored
+7
@@ -3084,6 +3084,7 @@
|
||||
"Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.": "サイトのオリジンのみを入力してください。例: https://api.example.com。/api/user/epay/notify などのパスは含めないでください。空欄の場合はサーバーアドレスを使用します。",
|
||||
"Only Mine": "自分のみ",
|
||||
"Only one catch-all route is allowed for the same incoming path": "同じ入力パスではキャッチオールルートは1つだけ許可されます",
|
||||
"Only one OpenAI Models route is allowed": "OpenAI モデルルートは 1 つだけ設定できます",
|
||||
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "選択されたフィールドのみが上書きされます。新しい競合が発生した場合は、同期ウィザードを再実行できます。",
|
||||
"Only successful requests": "成功したリクエストのみ",
|
||||
"Only successful requests count toward this limit.": "成功したリクエストのみがこの制限にカウントされます。",
|
||||
@@ -3116,6 +3117,11 @@
|
||||
"OpenAI Embeddings": "OpenAI 埋め込み",
|
||||
"OpenAI Image Edits": "OpenAI 画像編集",
|
||||
"OpenAI Image Generations": "OpenAI 画像生成",
|
||||
"OpenAI Models": "OpenAI モデル",
|
||||
"OpenAI Models route does not support client model rules": "OpenAI モデルルートはクライアントモデルルールに対応していません",
|
||||
"OpenAI Models route is required to enable upstream model checks": "アップストリームモデルの確認を有効にするには OpenAI モデルルートが必要です",
|
||||
"OpenAI Models route must use native forwarding": "OpenAI モデルルートではネイティブ転送を使用する必要があります",
|
||||
"OpenAI Models upstream path must not contain {model}": "OpenAI モデルのアップストリームパスに {model} を含めることはできません",
|
||||
"OpenAI Organization": "OpenAI組織",
|
||||
"OpenAI Organization ID (optional)": "OpenAI 組織 ID (オプション)",
|
||||
"OpenAI Realtime": "OpenAI リアルタイム",
|
||||
@@ -4528,6 +4534,7 @@
|
||||
"This plan does not allow balance redemption": "このプランでは残高での交換は許可されていません",
|
||||
"This project must be used in compliance with the": "このプロジェクトは、以下を遵守して使用する必要があります",
|
||||
"This removes {{count}} failed models from this channel. This action cannot be undone.": "この操作はこのチャンネルから失敗した {{count}} 個のモデルを削除します。元に戻せません。",
|
||||
"This route discovers upstream OpenAI models and cannot be split or matched by client model rules.": "このルートはアップストリームの OpenAI モデルを検出するためのもので、分割やクライアントモデルルールによる照合はできません。",
|
||||
"This site currently has {{count}} models enabled": "このサイトでは現在 {{count}} 個のモデルが有効です",
|
||||
"This tier catches any request that did not match earlier tiers.": "この段階は、前の段階に一致しなかったすべてのリクエストを受け取ります。",
|
||||
"this token group": "このトークングループ",
|
||||
|
||||
Vendored
+7
@@ -3084,6 +3084,7 @@
|
||||
"Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.": "Введите только origin сайта, например https://api.example.com. Не добавляйте пути, например /api/user/epay/notify. Оставьте пустым, чтобы использовать адрес сервера.",
|
||||
"Only Mine": "Только мои",
|
||||
"Only one catch-all route is allowed for the same incoming path": "Для одного входного пути разрешен только один резервный маршрут",
|
||||
"Only one OpenAI Models route is allowed": "Допускается только один маршрут моделей OpenAI",
|
||||
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Будут перезаписаны только выбранные поля. Вы можете повторно запустить мастер синхронизации, если появятся новые конфликты.",
|
||||
"Only successful requests": "Только успешные запросы",
|
||||
"Only successful requests count toward this limit.": "Только успешные запросы учитываются в этом лимите.",
|
||||
@@ -3116,6 +3117,11 @@
|
||||
"OpenAI Embeddings": "Векторные представления OpenAI",
|
||||
"OpenAI Image Edits": "Редактирование изображений OpenAI",
|
||||
"OpenAI Image Generations": "Генерация изображений OpenAI",
|
||||
"OpenAI Models": "Модели OpenAI",
|
||||
"OpenAI Models route does not support client model rules": "Маршрут моделей OpenAI не поддерживает правила клиентских моделей",
|
||||
"OpenAI Models route is required to enable upstream model checks": "Для проверки моделей вышестоящего сервиса требуется маршрут моделей OpenAI",
|
||||
"OpenAI Models route must use native forwarding": "Маршрут моделей OpenAI должен использовать прямую передачу",
|
||||
"OpenAI Models upstream path must not contain {model}": "Путь вышестоящего сервиса для моделей OpenAI не должен содержать {model}",
|
||||
"OpenAI Organization": "Организация OpenAI",
|
||||
"OpenAI Organization ID (optional)": "Идентификатор организации OpenAI (необязательно)",
|
||||
"OpenAI Realtime": "Реальное время OpenAI",
|
||||
@@ -4528,6 +4534,7 @@
|
||||
"This plan does not allow balance redemption": "Этот план не разрешает оплату балансом",
|
||||
"This project must be used in compliance with the": "Этот проект должен использоваться в соответствии с",
|
||||
"This removes {{count}} failed models from this channel. This action cannot be undone.": "Это удалит {{count}} неуспешных моделей из этого канала. Действие необратимо.",
|
||||
"This route discovers upstream OpenAI models and cannot be split or matched by client model rules.": "Этот маршрут обнаруживает модели OpenAI вышестоящего сервиса; его нельзя разделять или сопоставлять по правилам клиентских моделей.",
|
||||
"This site currently has {{count}} models enabled": "На этом сайте сейчас включено моделей: {{count}}",
|
||||
"This tier catches any request that did not match earlier tiers.": "Этот уровень обрабатывает все запросы, которые не совпали с предыдущими уровнями.",
|
||||
"this token group": "эта группа токенов",
|
||||
|
||||
Vendored
+7
@@ -3084,6 +3084,7 @@
|
||||
"Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.": "Chỉ nhập origin của trang, ví dụ https://api.example.com. Không nhập đường dẫn như /api/user/epay/notify. Để trống để dùng địa chỉ máy chủ.",
|
||||
"Only Mine": "Chỉ của tôi",
|
||||
"Only one catch-all route is allowed for the same incoming path": "Mỗi đường dẫn đầu vào chỉ được có một tuyến dự phòng",
|
||||
"Only one OpenAI Models route is allowed": "Chỉ được phép có một tuyến Mô hình OpenAI",
|
||||
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "Chỉ các trường được chọn sẽ bị ghi đè. Bạn có thể chạy lại trình hướng dẫn đồng bộ hóa nếu có xung đột mới xuất hiện.",
|
||||
"Only successful requests": "Chỉ các yêu cầu thành công",
|
||||
"Only successful requests count toward this limit.": "Chỉ những yêu cầu thành công mới được tính vào giới hạn này.",
|
||||
@@ -3116,6 +3117,11 @@
|
||||
"OpenAI Embeddings": "OpenAI Embeddings",
|
||||
"OpenAI Image Edits": "OpenAI Image Edits",
|
||||
"OpenAI Image Generations": "OpenAI Image Generations",
|
||||
"OpenAI Models": "Mô hình OpenAI",
|
||||
"OpenAI Models route does not support client model rules": "Tuyến Mô hình OpenAI không hỗ trợ quy tắc mô hình phía máy khách",
|
||||
"OpenAI Models route is required to enable upstream model checks": "Cần có tuyến Mô hình OpenAI để bật kiểm tra mô hình thượng nguồn",
|
||||
"OpenAI Models route must use native forwarding": "Tuyến Mô hình OpenAI phải dùng chuyển tiếp nguyên bản",
|
||||
"OpenAI Models upstream path must not contain {model}": "Đường dẫn thượng nguồn của Mô hình OpenAI không được chứa {model}",
|
||||
"OpenAI Organization": "Tổ chức OpenAI",
|
||||
"OpenAI Organization ID (optional)": "ID Tổ chức OpenAI (tùy chọn)",
|
||||
"OpenAI Realtime": "OpenAI Realtime",
|
||||
@@ -4528,6 +4534,7 @@
|
||||
"This plan does not allow balance redemption": "Gói này không cho phép thanh toán bằng số dư",
|
||||
"This project must be used in compliance with the": "Dự án này phải được sử dụng tuân thủ theo",
|
||||
"This removes {{count}} failed models from this channel. This action cannot be undone.": "Thao tác này sẽ xóa {{count}} mô hình thất bại khỏi kênh này. Không thể hoàn tác.",
|
||||
"This route discovers upstream OpenAI models and cannot be split or matched by client model rules.": "Tuyến này khám phá các mô hình OpenAI thượng nguồn và không thể tách hoặc đối sánh bằng quy tắc mô hình phía máy khách.",
|
||||
"This site currently has {{count}} models enabled": "Trang này hiện đã bật {{count}} mô hình",
|
||||
"This tier catches any request that did not match earlier tiers.": "Tầng này bắt mọi yêu cầu không khớp với các tầng trước.",
|
||||
"this token group": "nhóm token này",
|
||||
|
||||
+7
@@ -3084,6 +3084,7 @@
|
||||
"Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.": "只填寫站點根域名,例如 https://api.example.com。不要填寫 /api/user/epay/notify 這類路徑。留空則使用伺服器地址。",
|
||||
"Only Mine": "僅自己",
|
||||
"Only one catch-all route is allowed for the same incoming path": "同一入口路徑只允許一個兜底路由",
|
||||
"Only one OpenAI Models route is allowed": "僅允許設定一條 OpenAI 模型路由",
|
||||
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "僅選定的欄位將會被覆蓋。如果出現新的衝突,您可以重新執行同步精靈。",
|
||||
"Only successful requests": "僅成功的請求",
|
||||
"Only successful requests count toward this limit.": "僅成功的請求計入此限制。",
|
||||
@@ -3116,6 +3117,11 @@
|
||||
"OpenAI Embeddings": "OpenAI 嵌入",
|
||||
"OpenAI Image Edits": "OpenAI 圖像編輯",
|
||||
"OpenAI Image Generations": "OpenAI 圖像生成",
|
||||
"OpenAI Models": "OpenAI 模型",
|
||||
"OpenAI Models route does not support client model rules": "OpenAI 模型路由不支援用戶端模型規則",
|
||||
"OpenAI Models route is required to enable upstream model checks": "啟用上游模型檢查必須設定 OpenAI 模型路由",
|
||||
"OpenAI Models route must use native forwarding": "OpenAI 模型路由必須使用原生轉發",
|
||||
"OpenAI Models upstream path must not contain {model}": "OpenAI 模型上游路徑不得包含 {model}",
|
||||
"OpenAI Organization": "OpenAI 組織",
|
||||
"OpenAI Organization ID (optional)": "OpenAI 組織 ID(可選)",
|
||||
"OpenAI Realtime": "OpenAI 實時",
|
||||
@@ -4528,6 +4534,7 @@
|
||||
"This plan does not allow balance redemption": "該套餐不允許使用餘額兌換",
|
||||
"This project must be used in compliance with the": "此項目的使用必須遵守",
|
||||
"This removes {{count}} failed models from this channel. This action cannot be undone.": "此操作將從該渠道移除 {{count}} 個測試失敗的模型,且無法撤銷。",
|
||||
"This route discovers upstream OpenAI models and cannot be split or matched by client model rules.": "此路由用於探索上游 OpenAI 模型,無法拆分或使用用戶端模型規則配對。",
|
||||
"This site currently has {{count}} models enabled": "本站目前已啟用模型,總計 {{count}} 個",
|
||||
"This tier catches any request that did not match earlier tiers.": "此階梯會兜底處理未匹配前面階梯的請求。",
|
||||
"this token group": "此令牌分組",
|
||||
|
||||
Vendored
+7
@@ -3084,6 +3084,7 @@
|
||||
"Only enter the site origin, for example https://api.example.com. Do not include any path such as /api/user/epay/notify. Leave blank to use the server address.": "只填写站点根域名,例如 https://api.example.com。不要填写 /api/user/epay/notify 这类路径。留空则使用服务器地址。",
|
||||
"Only Mine": "仅自己",
|
||||
"Only one catch-all route is allowed for the same incoming path": "同一入口路径只允许一个兜底路由",
|
||||
"Only one OpenAI Models route is allowed": "仅允许配置一条 OpenAI 模型路由",
|
||||
"Only selected fields will be overwritten. You can re-run the sync wizard if new conflicts appear.": "仅选定的字段将被覆盖。如果出现新的冲突,您可以重新运行同步向导。",
|
||||
"Only successful requests": "仅成功的请求",
|
||||
"Only successful requests count toward this limit.": "仅成功的请求计入此限制。",
|
||||
@@ -3116,6 +3117,11 @@
|
||||
"OpenAI Embeddings": "OpenAI 嵌入",
|
||||
"OpenAI Image Edits": "OpenAI 图像编辑",
|
||||
"OpenAI Image Generations": "OpenAI 图像生成",
|
||||
"OpenAI Models": "OpenAI 模型",
|
||||
"OpenAI Models route does not support client model rules": "OpenAI 模型路由不支持客户端模型规则",
|
||||
"OpenAI Models route is required to enable upstream model checks": "启用上游模型检查必须配置 OpenAI 模型路由",
|
||||
"OpenAI Models route must use native forwarding": "OpenAI 模型路由必须使用原生转发",
|
||||
"OpenAI Models upstream path must not contain {model}": "OpenAI 模型上游路径不能包含 {model}",
|
||||
"OpenAI Organization": "OpenAI 组织",
|
||||
"OpenAI Organization ID (optional)": "OpenAI 组织 ID(可选)",
|
||||
"OpenAI Realtime": "OpenAI 实时",
|
||||
@@ -4528,6 +4534,7 @@
|
||||
"This plan does not allow balance redemption": "该套餐不允许使用余额兑换",
|
||||
"This project must be used in compliance with the": "此项目的使用必须遵守",
|
||||
"This removes {{count}} failed models from this channel. This action cannot be undone.": "此操作将从该渠道移除 {{count}} 个测试失败的模型,且无法撤销。",
|
||||
"This route discovers upstream OpenAI models and cannot be split or matched by client model rules.": "此路由用于发现上游 OpenAI 模型,不能拆分或使用客户端模型规则匹配。",
|
||||
"This site currently has {{count}} models enabled": "本站当前已启用模型,总计 {{count}} 个",
|
||||
"This tier catches any request that did not match earlier tiers.": "此阶梯会兜底处理未匹配前面阶梯的请求。",
|
||||
"this token group": "此令牌分组",
|
||||
|
||||
Vendored
+8
@@ -530,6 +530,14 @@ export const STATIC_I18N_KEYS = [
|
||||
'Batch detection failed',
|
||||
'Batch detection complete: {{channels}} channels, {{add}} to add, {{remove}} to remove, {{fails}} failed',
|
||||
|
||||
// Advanced Custom model discovery
|
||||
'OpenAI Models',
|
||||
'Only one OpenAI Models route is allowed',
|
||||
'OpenAI Models route does not support client model rules',
|
||||
'OpenAI Models route must use native forwarding',
|
||||
'OpenAI Models upstream path must not contain {model}',
|
||||
'OpenAI Models route is required to enable upstream model checks',
|
||||
|
||||
// Dashboard flow stages (labels/descriptions passed to t at runtime)
|
||||
'User',
|
||||
'Node',
|
||||
|
||||
Reference in New Issue
Block a user