feat: enhance text protocol conversion and advanced custom routing (#5825)
* refactor: consolidate relay protocol converters * refactor relayconvert text converters * feat: refine relay converters and advanced custom routing * refactor: enhance logging and add thought signature handling for Gemini requests * refactor: enhance channel cache and pricing endpoint handling for advanced custom models * feat: preserve billing usage semantics * feat: add protocol-aware billing usage * Delete useless files * chore: update action versions in workflow files * chore: update Docker action versions in workflow files * fix: harden billing usage settlement and hot-path route matching - estimate Gemini completion tokens locally when billable usageMetadata is prompt-only but output content was received (e.g. client aborts the stream before the final chunk), and rebuild the attached billing_usage as estimated so settlement does not bill zero output tokens - guard NewClaudeMessagesBillingUsage against all-zero ClaudeUsage, matching the OpenAI/Gemini constructors, so a zero billing_usage cannot override a non-zero top-level usage during settlement - cache compiled advanced-custom route model regexes; they run on the request hot path and were recompiled per request - move the effectiveBillingUsage remap to PostTextConsumeQuota only, and document that calculateTextQuotaSummary expects remapped usage - document the updatePricingLock -> channelSyncLock lock ordering that InitChannelCache/CacheUpdateChannel rely on, and the aux-struct pitfall in GeminiChatResponse.UnmarshalJSON
This commit is contained in:
+658
-109
@@ -16,15 +16,35 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import { ArrowRight, Check, Plus, Shuffle, Trash2 } from 'lucide-react'
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowDownToLine,
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
Check,
|
||||
Info,
|
||||
Plus,
|
||||
Shuffle,
|
||||
Trash2,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react'
|
||||
import { type ReactNode, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { Dialog } from '@/components/dialog'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverDescription,
|
||||
PopoverHeader,
|
||||
PopoverTitle,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -53,13 +73,17 @@ import {
|
||||
createAdvancedCustomConfig,
|
||||
createAdvancedCustomRoute,
|
||||
getAdvancedCustomAuthMode,
|
||||
getAdvancedCustomConverterDefaults,
|
||||
getAdvancedCustomConverterOptions,
|
||||
getAdvancedCustomIncomingPathLabel,
|
||||
getAdvancedCustomModelRuleKind,
|
||||
getAdvancedCustomRegexModelPattern,
|
||||
getAdvancedCustomTemplateConfig,
|
||||
getAdvancedCustomUpstreamPathPlaceholder,
|
||||
getDefaultAdvancedCustomIncomingPath,
|
||||
isAdvancedCustomIncomingPathAllowed,
|
||||
normalizeAdvancedCustomConfig,
|
||||
parseAdvancedCustomRouteModels,
|
||||
parseAdvancedCustomConfig,
|
||||
stringifyAdvancedCustomConfig,
|
||||
validateAdvancedCustomConfig,
|
||||
@@ -84,9 +108,23 @@ const longSelectContentClass = 'w-[360px] max-w-[calc(100vw-2rem)]'
|
||||
const longSelectItemClass =
|
||||
'items-start py-2 [&_[data-slot=select-item-text]]:min-w-0 [&_[data-slot=select-item-text]]:shrink [&_[data-slot=select-item-text]]:whitespace-normal'
|
||||
const routeEditorGridClassName =
|
||||
'lg:grid-cols-[7rem_minmax(0,1.45fr)_minmax(0,1.35fr)_minmax(0,1fr)_minmax(0,0.85fr)_2rem]'
|
||||
'lg:grid-cols-[6rem_minmax(0,1fr)_minmax(0,1.25fr)_minmax(0,1fr)_minmax(0,0.85fr)_7rem]'
|
||||
const upstreamPathDescriptionKey =
|
||||
'Use a path to append it to the channel Base URL, or enter a full URL to override the Base URL for this route.'
|
||||
const catchAllOrderErrorMessage =
|
||||
'Catch-all route must be last for the same incoming path'
|
||||
const emptyAdvancedRoutes: AdvancedCustomRoute[] = []
|
||||
|
||||
type AdvancedCustomRouteRow = {
|
||||
route: AdvancedCustomRoute
|
||||
routeKey: string
|
||||
index: number
|
||||
}
|
||||
|
||||
type AdvancedCustomRouteGroup = {
|
||||
incomingPath: string
|
||||
routeRows: AdvancedCustomRouteRow[]
|
||||
}
|
||||
|
||||
function getOptionLabel(
|
||||
options: ReadonlyArray<{ value: string; label: string }>,
|
||||
@@ -95,6 +133,34 @@ function getOptionLabel(
|
||||
return options.find((option) => option.value === value)?.label || value
|
||||
}
|
||||
|
||||
function getRouteIncomingPath(route: AdvancedCustomRoute): string {
|
||||
return (route.incoming_path || '').trim()
|
||||
}
|
||||
|
||||
function isCatchAllRoute(route: AdvancedCustomRoute): boolean {
|
||||
return !route.models || route.models.length === 0
|
||||
}
|
||||
|
||||
function buildRouteGroups(
|
||||
routeRows: AdvancedCustomRouteRow[]
|
||||
): AdvancedCustomRouteGroup[] {
|
||||
const groups: AdvancedCustomRouteGroup[] = []
|
||||
const groupByPath = new Map<string, AdvancedCustomRouteGroup>()
|
||||
|
||||
for (const routeRow of routeRows) {
|
||||
const incomingPath = getRouteIncomingPath(routeRow.route)
|
||||
let group = groupByPath.get(incomingPath)
|
||||
if (!group) {
|
||||
group = { incomingPath, routeRows: [] }
|
||||
groupByPath.set(incomingPath, group)
|
||||
groups.push(group)
|
||||
}
|
||||
group.routeRows.push(routeRow)
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
export function AdvancedCustomEditorDialog({
|
||||
open,
|
||||
value,
|
||||
@@ -133,20 +199,28 @@ export function AdvancedCustomEditorDialog({
|
||||
() => normalizeAdvancedCustomConfig(config),
|
||||
[config]
|
||||
)
|
||||
const routes = normalizedConfig.advanced_routes || []
|
||||
const routeRows = routes.map((route, index) => ({
|
||||
route,
|
||||
routeKey:
|
||||
routeKeys.at(index) ||
|
||||
route.incoming_path ||
|
||||
route.upstream_path ||
|
||||
route.converter ||
|
||||
'advanced-custom-route',
|
||||
}))
|
||||
const routes = normalizedConfig.advanced_routes || emptyAdvancedRoutes
|
||||
const routeRows = useMemo(
|
||||
() =>
|
||||
routes.map((route, index) => ({
|
||||
route,
|
||||
index,
|
||||
routeKey:
|
||||
routeKeys.at(index) ||
|
||||
route.incoming_path ||
|
||||
route.upstream_path ||
|
||||
route.converter ||
|
||||
'advanced-custom-route',
|
||||
})),
|
||||
[routeKeys, routes]
|
||||
)
|
||||
const routeGroups = useMemo(() => buildRouteGroups(routeRows), [routeRows])
|
||||
const validationError = useMemo(
|
||||
() => validateAdvancedCustomConfig(normalizedConfig),
|
||||
[normalizedConfig]
|
||||
)
|
||||
const canFixCatchAllOrder =
|
||||
validationError?.message === catchAllOrderErrorMessage
|
||||
|
||||
const createRouteKey = () => {
|
||||
routeKeyCounterRef.current += 1
|
||||
@@ -165,6 +239,17 @@ export function AdvancedCustomEditorDialog({
|
||||
})
|
||||
}
|
||||
|
||||
const replaceRoutes = (
|
||||
nextRoutes: AdvancedCustomRoute[],
|
||||
nextRouteKeys = routeRows.map((routeRow) => routeRow.routeKey)
|
||||
) => {
|
||||
setConfig((current) => {
|
||||
const next = normalizeAdvancedCustomConfig(current)
|
||||
return { ...next, advanced_routes: nextRoutes }
|
||||
})
|
||||
setRouteKeys(nextRouteKeys)
|
||||
}
|
||||
|
||||
const addRoute = () => {
|
||||
setConfig((current) => {
|
||||
const next = normalizeAdvancedCustomConfig(current)
|
||||
@@ -179,6 +264,25 @@ export function AdvancedCustomEditorDialog({
|
||||
setRouteKeys((current) => [...current, createRouteKey()])
|
||||
}
|
||||
|
||||
const addRouteForIncomingPath = (incomingPath: string) => {
|
||||
const resolvedIncomingPath = incomingPath || '/v1/chat/completions'
|
||||
setConfig((current) => {
|
||||
const next = normalizeAdvancedCustomConfig(current)
|
||||
return {
|
||||
...next,
|
||||
advanced_routes: [
|
||||
...(next.advanced_routes || []),
|
||||
{
|
||||
...createAdvancedCustomRoute(),
|
||||
incoming_path: resolvedIncomingPath,
|
||||
upstream_path: resolvedIncomingPath,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
setRouteKeys((current) => [...current, createRouteKey()])
|
||||
}
|
||||
|
||||
const removeRoute = (index: number) => {
|
||||
setConfig((current) => {
|
||||
const next = normalizeAdvancedCustomConfig(current)
|
||||
@@ -194,6 +298,105 @@ export function AdvancedCustomEditorDialog({
|
||||
)
|
||||
}
|
||||
|
||||
const updateGroupIncomingPath = (
|
||||
group: AdvancedCustomRouteGroup,
|
||||
nextIncomingPath: string | null
|
||||
) => {
|
||||
const resolvedIncomingPath = nextIncomingPath || '/v1/chat/completions'
|
||||
const groupRouteIndexes = new Set(
|
||||
group.routeRows.map((routeRow) => routeRow.index)
|
||||
)
|
||||
const nextRoutes = routes.map((route, routeIndex) => {
|
||||
if (!groupRouteIndexes.has(routeIndex)) return route
|
||||
const converter = route.converter || 'none'
|
||||
return {
|
||||
...route,
|
||||
incoming_path: resolvedIncomingPath,
|
||||
converter: isAdvancedCustomIncomingPathAllowed(
|
||||
resolvedIncomingPath,
|
||||
converter
|
||||
)
|
||||
? converter
|
||||
: 'none',
|
||||
}
|
||||
})
|
||||
replaceRoutes(nextRoutes)
|
||||
}
|
||||
|
||||
const swapRoutes = (fromIndex: number, toIndex: number) => {
|
||||
if (fromIndex === toIndex) return
|
||||
const nextRoutes = [...routes]
|
||||
const nextRouteKeys = routeRows.map((routeRow) => routeRow.routeKey)
|
||||
const fromRoute = nextRoutes[fromIndex]
|
||||
nextRoutes[fromIndex] = nextRoutes[toIndex]
|
||||
nextRoutes[toIndex] = fromRoute
|
||||
const fromRouteKey = nextRouteKeys[fromIndex]
|
||||
nextRouteKeys[fromIndex] = nextRouteKeys[toIndex]
|
||||
nextRouteKeys[toIndex] = fromRouteKey
|
||||
replaceRoutes(nextRoutes, nextRouteKeys)
|
||||
}
|
||||
|
||||
const moveRouteWithinGroup = (index: number, direction: -1 | 1) => {
|
||||
const incomingPath = getRouteIncomingPath(routes[index])
|
||||
const samePathIndexes = routes
|
||||
.map((route, routeIndex) => ({ route, routeIndex }))
|
||||
.filter(({ route }) => getRouteIncomingPath(route) === incomingPath)
|
||||
.map(({ routeIndex }) => routeIndex)
|
||||
const position = samePathIndexes.indexOf(index)
|
||||
const nextIndex = samePathIndexes.at(position + direction)
|
||||
if (nextIndex === undefined) return
|
||||
swapRoutes(index, nextIndex)
|
||||
}
|
||||
|
||||
const moveRouteToGroupEnd = (index: number) => {
|
||||
const incomingPath = getRouteIncomingPath(routes[index])
|
||||
let lastSamePathIndex = -1
|
||||
for (let routeIndex = routes.length - 1; routeIndex >= 0; routeIndex -= 1) {
|
||||
if (getRouteIncomingPath(routes[routeIndex]) === incomingPath) {
|
||||
lastSamePathIndex = routeIndex
|
||||
break
|
||||
}
|
||||
}
|
||||
if (lastSamePathIndex < 0 || index === lastSamePathIndex) return
|
||||
|
||||
const nextRoutes = [...routes]
|
||||
const nextRouteKeys = routeRows.map((routeRow) => routeRow.routeKey)
|
||||
const [route] = nextRoutes.splice(index, 1)
|
||||
const [routeKey] = nextRouteKeys.splice(index, 1)
|
||||
nextRoutes.splice(lastSamePathIndex, 0, route)
|
||||
nextRouteKeys.splice(lastSamePathIndex, 0, routeKey)
|
||||
replaceRoutes(nextRoutes, nextRouteKeys)
|
||||
}
|
||||
|
||||
const fixCatchAllOrder = () => {
|
||||
const routeRowsByPath = new Map<string, AdvancedCustomRouteRow[]>()
|
||||
for (const routeRow of routeRows) {
|
||||
const incomingPath = getRouteIncomingPath(routeRow.route)
|
||||
routeRowsByPath.set(incomingPath, [
|
||||
...(routeRowsByPath.get(incomingPath) || []),
|
||||
routeRow,
|
||||
])
|
||||
}
|
||||
|
||||
const orderedRowsByPath = new Map<string, AdvancedCustomRouteRow[]>()
|
||||
for (const [incomingPath, rows] of routeRowsByPath) {
|
||||
orderedRowsByPath.set(incomingPath, [
|
||||
...rows.filter((routeRow) => !isCatchAllRoute(routeRow.route)),
|
||||
...rows.filter((routeRow) => isCatchAllRoute(routeRow.route)),
|
||||
])
|
||||
}
|
||||
|
||||
const nextRows = routeRows.map((routeRow) => {
|
||||
const incomingPath = getRouteIncomingPath(routeRow.route)
|
||||
const orderedRows = orderedRowsByPath.get(incomingPath)
|
||||
return orderedRows?.shift() || routeRow
|
||||
})
|
||||
replaceRoutes(
|
||||
nextRows.map((routeRow) => routeRow.route),
|
||||
nextRows.map((routeRow) => routeRow.routeKey)
|
||||
)
|
||||
}
|
||||
|
||||
const parseJsonEditorConfig = (): AdvancedCustomConfig | null => {
|
||||
const parsed = parseAdvancedCustomConfig(jsonText)
|
||||
if (!parsed) {
|
||||
@@ -302,7 +505,7 @@ export function AdvancedCustomEditorDialog({
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button type='button' onClick={saveConfig}>
|
||||
<Check className='mr-2 h-4 w-4' />
|
||||
<Check data-icon='inline-start' />
|
||||
{t('Save changes')}
|
||||
</Button>
|
||||
</>
|
||||
@@ -395,18 +598,30 @@ export function AdvancedCustomEditorDialog({
|
||||
size='sm'
|
||||
onClick={addRoute}
|
||||
>
|
||||
<Plus className='mr-2 h-4 w-4' />
|
||||
<Plus data-icon='inline-start' />
|
||||
{t('Add route')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{validationError ? (
|
||||
<Alert variant='destructive'>
|
||||
<AlertDescription>
|
||||
{validationError.routeIndex !== undefined
|
||||
? `${t('Route')} ${validationError.routeIndex + 1}: `
|
||||
: ''}
|
||||
{t(validationError.message)}
|
||||
<AlertDescription className='flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<span>
|
||||
{validationError.routeIndex !== undefined
|
||||
? `${t('Route')} ${validationError.routeIndex + 1}: `
|
||||
: ''}
|
||||
{t(validationError.message)}
|
||||
</span>
|
||||
{canFixCatchAllOrder ? (
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={fixCatchAllOrder}
|
||||
>
|
||||
{t('Fix order')}
|
||||
</Button>
|
||||
) : null}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
@@ -415,27 +630,24 @@ export function AdvancedCustomEditorDialog({
|
||||
{t(upstreamPathDescriptionKey)}
|
||||
</p>
|
||||
|
||||
<div className='flex flex-col gap-4 lg:gap-2'>
|
||||
<div
|
||||
className={cn(
|
||||
'text-muted-foreground hidden items-center gap-2 px-3 text-xs font-medium lg:grid',
|
||||
routeEditorGridClassName
|
||||
)}
|
||||
>
|
||||
<span>{t('Route')}</span>
|
||||
<span>{t('Incoming path')}</span>
|
||||
<span>{t('Upstream path')}</span>
|
||||
<span>{t('Converter')}</span>
|
||||
<span>{t('Auth')}</span>
|
||||
<span aria-hidden='true' />
|
||||
</div>
|
||||
{routeRows.map((routeRow, index) => (
|
||||
<RouteEditor
|
||||
key={routeRow.routeKey}
|
||||
route={routeRow.route}
|
||||
index={index}
|
||||
onChange={(patch) => updateRoute(index, patch)}
|
||||
onRemove={() => removeRoute(index)}
|
||||
<div className='flex flex-col gap-4'>
|
||||
{routeGroups.map((routeGroup) => (
|
||||
<RouteGroupEditor
|
||||
key={routeGroup.incomingPath || 'advanced-custom-empty-path'}
|
||||
group={routeGroup}
|
||||
validationError={validationError}
|
||||
onAddRoute={() =>
|
||||
addRouteForIncomingPath(routeGroup.incomingPath)
|
||||
}
|
||||
onIncomingPathChange={(nextIncomingPath) =>
|
||||
updateGroupIncomingPath(routeGroup, nextIncomingPath)
|
||||
}
|
||||
onMoveRoute={(index, direction) =>
|
||||
moveRouteWithinGroup(index, direction)
|
||||
}
|
||||
onMoveRouteToEnd={moveRouteToGroupEnd}
|
||||
onRemoveRoute={removeRoute}
|
||||
onRouteChange={updateRoute}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -476,15 +688,191 @@ export function AdvancedCustomEditorDialog({
|
||||
)
|
||||
}
|
||||
|
||||
function RouteGroupEditor({
|
||||
group,
|
||||
validationError,
|
||||
onAddRoute,
|
||||
onIncomingPathChange,
|
||||
onMoveRoute,
|
||||
onMoveRouteToEnd,
|
||||
onRemoveRoute,
|
||||
onRouteChange,
|
||||
}: {
|
||||
group: AdvancedCustomRouteGroup
|
||||
validationError: ReturnType<typeof validateAdvancedCustomConfig>
|
||||
onAddRoute: () => void
|
||||
onIncomingPathChange: (incomingPath: string | null) => void
|
||||
onMoveRoute: (index: number, direction: -1 | 1) => void
|
||||
onMoveRouteToEnd: (index: number) => void
|
||||
onRemoveRoute: (index: number) => void
|
||||
onRouteChange: (index: number, patch: Partial<AdvancedCustomRoute>) => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const incomingPath = group.incomingPath || '/v1/chat/completions'
|
||||
const incomingPathLabel = getAdvancedCustomIncomingPathLabel(incomingPath)
|
||||
const catchAllRoute = group.routeRows.find((routeRow) =>
|
||||
isCatchAllRoute(routeRow.route)
|
||||
)
|
||||
const catchAllRoutePosition = catchAllRoute
|
||||
? group.routeRows.findIndex(
|
||||
(routeRow) => routeRow.index === catchAllRoute.index
|
||||
)
|
||||
: -1
|
||||
const hasCatchAll = catchAllRoute !== undefined
|
||||
const catchAllIsLast =
|
||||
!hasCatchAll || catchAllRoutePosition === group.routeRows.length - 1
|
||||
const groupHasError =
|
||||
validationError?.routeIndex !== undefined &&
|
||||
group.routeRows.some(
|
||||
(routeRow) => routeRow.index === validationError.routeIndex
|
||||
)
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'border-border overflow-hidden rounded-md border',
|
||||
groupHasError && 'border-destructive/60'
|
||||
)}
|
||||
>
|
||||
<div className='bg-muted/20 flex flex-col gap-3 p-3 lg:flex-row lg:items-center lg:justify-between'>
|
||||
<div className='flex min-w-0 flex-1 flex-col gap-2'>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<span className='text-sm font-medium'>{t('Route group')}</span>
|
||||
<Badge variant='secondary'>
|
||||
{group.routeRows.length} {t('Routes')}
|
||||
</Badge>
|
||||
<Badge variant={hasCatchAll ? 'outline' : 'secondary'}>
|
||||
{hasCatchAll ? t('Fallback route') : t('Model-scoped only')}
|
||||
</Badge>
|
||||
{!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}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
alignItemWithTrigger={false}
|
||||
className={longSelectContentClass}
|
||||
>
|
||||
<SelectGroup>
|
||||
{ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS.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>{option.label}</span>
|
||||
<span className='text-muted-foreground font-mono text-xs break-all'>
|
||||
{option.value}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button type='button' variant='outline' size='sm' onClick={onAddRoute}>
|
||||
<Plus data-icon='inline-start' />
|
||||
{t('Add split')}
|
||||
</Button>
|
||||
</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.'
|
||||
)}
|
||||
</p>
|
||||
{groupHasError && validationError ? (
|
||||
<p className='text-destructive mt-1 text-xs'>
|
||||
{validationError.routeIndex !== undefined
|
||||
? `${t('Route')} ${validationError.routeIndex + 1}: `
|
||||
: ''}
|
||||
{t(validationError.message)}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'text-muted-foreground hidden items-center gap-2 border-t bg-muted/10 px-3 py-2 text-xs font-medium lg:grid',
|
||||
routeEditorGridClassName
|
||||
)}
|
||||
>
|
||||
<span>{t('Route')}</span>
|
||||
<span className='inline-flex items-center gap-1'>
|
||||
{t('Client model')}
|
||||
<ModelRuleHelpPopover />
|
||||
</span>
|
||||
<span>{t('Upstream path')}</span>
|
||||
<span>{t('Converter')}</span>
|
||||
<span>{t('Auth')}</span>
|
||||
<span className='text-right'>{t('Actions')}</span>
|
||||
</div>
|
||||
|
||||
<div className='divide-y'>
|
||||
{group.routeRows.map((routeRow, position) => {
|
||||
const canMoveUp = position > 0
|
||||
const canMoveDown = position < group.routeRows.length - 1
|
||||
const catchAllOutOfOrder =
|
||||
isCatchAllRoute(routeRow.route) && canMoveDown
|
||||
const routeErrorMessage =
|
||||
validationError?.routeIndex === routeRow.index
|
||||
? validationError.message
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<RouteEditor
|
||||
key={routeRow.routeKey}
|
||||
route={routeRow.route}
|
||||
index={routeRow.index}
|
||||
errorMessage={routeErrorMessage}
|
||||
canMoveUp={canMoveUp}
|
||||
canMoveDown={canMoveDown}
|
||||
catchAllOutOfOrder={catchAllOutOfOrder}
|
||||
onChange={(patch) => onRouteChange(routeRow.index, patch)}
|
||||
onMoveDown={() => onMoveRoute(routeRow.index, 1)}
|
||||
onMoveUp={() => onMoveRoute(routeRow.index, -1)}
|
||||
onMoveCatchAllToEnd={() => onMoveRouteToEnd(routeRow.index)}
|
||||
onRemove={() => onRemoveRoute(routeRow.index)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function RouteEditor({
|
||||
route,
|
||||
index,
|
||||
errorMessage,
|
||||
canMoveUp,
|
||||
canMoveDown,
|
||||
catchAllOutOfOrder,
|
||||
onChange,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onMoveCatchAllToEnd,
|
||||
onRemove,
|
||||
}: {
|
||||
route: AdvancedCustomRoute
|
||||
index: number
|
||||
errorMessage?: string
|
||||
canMoveUp: boolean
|
||||
canMoveDown: boolean
|
||||
catchAllOutOfOrder: boolean
|
||||
onChange: (patch: Partial<AdvancedCustomRoute>) => void
|
||||
onMoveUp: () => void
|
||||
onMoveDown: () => void
|
||||
onMoveCatchAllToEnd: () => void
|
||||
onRemove: () => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
@@ -496,39 +884,52 @@ function RouteEditor({
|
||||
() => getAdvancedCustomConverterOptions(incomingPath),
|
||||
[incomingPath]
|
||||
)
|
||||
const incomingPathLabel = getAdvancedCustomIncomingPathLabel(incomingPath)
|
||||
const converterLabel = getOptionLabel(
|
||||
ADVANCED_CUSTOM_CONVERTER_OPTIONS,
|
||||
converter
|
||||
)
|
||||
const converterTriggerLabel =
|
||||
ADVANCED_CUSTOM_CONVERTER_OPTIONS.find(
|
||||
(option) => option.value === converter
|
||||
)?.triggerLabel || converterLabel
|
||||
const authLabel = getOptionLabel(ADVANCED_CUSTOM_AUTH_MODE_OPTIONS, authMode)
|
||||
const isNativeConverter = converter === 'none'
|
||||
const ConverterVisualIcon = isNativeConverter ? ArrowRight : Shuffle
|
||||
const modelsInputValue = route.models?.join(', ') || ''
|
||||
const parsedRouteModels = parseAdvancedCustomRouteModels(modelsInputValue)
|
||||
const isFallback = parsedRouteModels.length === 0
|
||||
|
||||
const setConverter = (nextConverter: AdvancedCustomConverter) => {
|
||||
const patch: Partial<AdvancedCustomRoute> = { converter: nextConverter }
|
||||
if (!isAdvancedCustomIncomingPathAllowed(incomingPath, nextConverter)) {
|
||||
patch.incoming_path = getDefaultAdvancedCustomIncomingPath(nextConverter)
|
||||
let nextIncomingPath = incomingPath
|
||||
if (!isAdvancedCustomIncomingPathAllowed(nextIncomingPath, nextConverter)) {
|
||||
nextIncomingPath = getDefaultAdvancedCustomIncomingPath(nextConverter)
|
||||
}
|
||||
onChange(patch)
|
||||
}
|
||||
|
||||
const setIncomingPath = (nextIncomingPath: string | null) => {
|
||||
const resolvedIncomingPath =
|
||||
nextIncomingPath || getDefaultAdvancedCustomIncomingPath(converter)
|
||||
const patch: Partial<AdvancedCustomRoute> = {
|
||||
incoming_path: resolvedIncomingPath,
|
||||
}
|
||||
if (!isAdvancedCustomIncomingPathAllowed(resolvedIncomingPath, converter)) {
|
||||
patch.converter = 'none'
|
||||
}
|
||||
onChange(patch)
|
||||
const defaults = getAdvancedCustomConverterDefaults(
|
||||
nextConverter,
|
||||
nextIncomingPath
|
||||
)
|
||||
onChange({
|
||||
converter: nextConverter,
|
||||
incoming_path: nextIncomingPath,
|
||||
upstream_path: defaults.upstream_path,
|
||||
auth: defaults.auth,
|
||||
})
|
||||
}
|
||||
|
||||
const setAuthMode = (mode: AdvancedCustomAuthMode) => {
|
||||
onChange({ auth: buildAdvancedCustomAuth(mode, route.auth) })
|
||||
}
|
||||
|
||||
const setModelsInput = (value: string) => {
|
||||
onChange({
|
||||
models: value === '' ? [] : value.split(','),
|
||||
})
|
||||
}
|
||||
|
||||
const normalizeModelsInput = (value: string) => {
|
||||
onChange({ models: parseAdvancedCustomRouteModels(value) })
|
||||
}
|
||||
|
||||
const updateAuth = (
|
||||
field: Exclude<keyof NonNullable<AdvancedCustomRoute['auth']>, 'type'>,
|
||||
value: string
|
||||
@@ -546,7 +947,12 @@ function RouteEditor({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='border-border flex flex-col gap-4 rounded-md border p-4 lg:gap-2 lg:p-3'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-4 px-4 py-4 lg:gap-2 lg:px-3 lg:py-3',
|
||||
errorMessage && 'bg-destructive/5'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'grid gap-4 md:grid-cols-2 lg:items-center lg:gap-2',
|
||||
@@ -559,6 +965,9 @@ function RouteEditor({
|
||||
<div className='text-sm font-medium'>
|
||||
{t('Route')} {index + 1}
|
||||
</div>
|
||||
{isFallback ? (
|
||||
<Badge variant='outline'>{t('Fallback')}</Badge>
|
||||
) : null}
|
||||
<TooltipProvider delay={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
@@ -586,51 +995,80 @@ function RouteEditor({
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='lg:hidden'
|
||||
onClick={onRemove}
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
<span className='sr-only'>{t('Delete')}</span>
|
||||
</Button>
|
||||
<div className='flex shrink-0 items-center gap-1 lg:hidden'>
|
||||
<TooltipIconButton
|
||||
label={t('Move route up')}
|
||||
icon={ArrowUp}
|
||||
disabled={!canMoveUp}
|
||||
onClick={onMoveUp}
|
||||
/>
|
||||
<TooltipIconButton
|
||||
label={t('Move route down')}
|
||||
icon={ArrowDown}
|
||||
disabled={!canMoveDown}
|
||||
onClick={onMoveDown}
|
||||
/>
|
||||
{catchAllOutOfOrder ? (
|
||||
<TooltipIconButton
|
||||
label={t('Move fallback to end')}
|
||||
icon={ArrowDownToLine}
|
||||
onClick={onMoveCatchAllToEnd}
|
||||
/>
|
||||
) : null}
|
||||
<TooltipIconButton
|
||||
label={t('Delete')}
|
||||
icon={Trash2}
|
||||
onClick={onRemove}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FieldBlock
|
||||
label={t('Incoming path')}
|
||||
label={
|
||||
<span className='inline-flex items-center gap-1'>
|
||||
{t('Client model')}
|
||||
<ModelRuleHelpPopover />
|
||||
</span>
|
||||
}
|
||||
className='lg:gap-1'
|
||||
labelClassName='lg:sr-only'
|
||||
>
|
||||
<Select value={incomingPath} onValueChange={setIncomingPath}>
|
||||
<SelectTrigger className='w-full max-w-full lg:h-8'>
|
||||
<SelectValue className='min-w-0 truncate'>
|
||||
{`${incomingPathLabel}`}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
alignItemWithTrigger={false}
|
||||
className={longSelectContentClass}
|
||||
>
|
||||
<SelectGroup>
|
||||
{ADVANCED_CUSTOM_INCOMING_PATH_OPTIONS.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={longSelectItemClass}
|
||||
<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'
|
||||
>
|
||||
<div className='flex min-w-0 flex-col gap-1 leading-snug whitespace-normal'>
|
||||
<span>{option.label}</span>
|
||||
<span className='text-muted-foreground font-mono text-xs break-all'>
|
||||
{option.value}
|
||||
</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<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
|
||||
@@ -645,7 +1083,10 @@ function RouteEditor({
|
||||
upstream_path: event.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={getAdvancedCustomUpstreamPathPlaceholder(converter)}
|
||||
placeholder={getAdvancedCustomUpstreamPathPlaceholder(
|
||||
converter,
|
||||
incomingPath
|
||||
)}
|
||||
/>
|
||||
<p className='text-muted-foreground text-xs leading-relaxed lg:hidden'>
|
||||
{t(upstreamPathDescriptionKey)}
|
||||
@@ -665,7 +1106,7 @@ function RouteEditor({
|
||||
>
|
||||
<SelectTrigger className='w-full max-w-full lg:h-8'>
|
||||
<SelectValue className='min-w-0 truncate'>
|
||||
{t(converterLabel)}
|
||||
{t(converterTriggerLabel)}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
@@ -717,18 +1158,38 @@ function RouteEditor({
|
||||
</Select>
|
||||
</FieldBlock>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='hidden lg:inline-flex'
|
||||
onClick={onRemove}
|
||||
>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
<span className='sr-only'>{t('Delete')}</span>
|
||||
</Button>
|
||||
<div className='hidden items-center justify-end gap-1 lg:flex'>
|
||||
<TooltipIconButton
|
||||
label={t('Move route up')}
|
||||
icon={ArrowUp}
|
||||
disabled={!canMoveUp}
|
||||
onClick={onMoveUp}
|
||||
/>
|
||||
<TooltipIconButton
|
||||
label={t('Move route down')}
|
||||
icon={ArrowDown}
|
||||
disabled={!canMoveDown}
|
||||
onClick={onMoveDown}
|
||||
/>
|
||||
{catchAllOutOfOrder ? (
|
||||
<TooltipIconButton
|
||||
label={t('Move fallback to end')}
|
||||
icon={ArrowDownToLine}
|
||||
onClick={onMoveCatchAllToEnd}
|
||||
/>
|
||||
) : null}
|
||||
<TooltipIconButton
|
||||
label={t('Delete')}
|
||||
icon={Trash2}
|
||||
onClick={onRemove}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errorMessage ? (
|
||||
<p className='text-destructive text-xs'>{t(errorMessage)}</p>
|
||||
) : null}
|
||||
|
||||
{authMode === 'header' || authMode === 'query' ? (
|
||||
<>
|
||||
<Separator className='lg:hidden' />
|
||||
@@ -775,13 +1236,101 @@ function RouteEditor({
|
||||
)
|
||||
}
|
||||
|
||||
function ModelRuleHelpPopover() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='text-muted-foreground hover:text-foreground size-6'
|
||||
aria-label={t('Client model matching help')}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Info className='size-3.5' aria-hidden='true' />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align='start'
|
||||
side='bottom'
|
||||
sideOffset={8}
|
||||
className='w-[min(22rem,calc(100vw-2rem))] gap-3 p-3'
|
||||
>
|
||||
<PopoverHeader className='gap-1'>
|
||||
<PopoverTitle>{t('Client model matching')}</PopoverTitle>
|
||||
<PopoverDescription className='text-xs leading-relaxed'>
|
||||
{t(
|
||||
'Rules match the original model value from the client request body.'
|
||||
)}
|
||||
</PopoverDescription>
|
||||
</PopoverHeader>
|
||||
<div className='text-muted-foreground space-y-2 text-xs leading-relaxed'>
|
||||
<p>
|
||||
{t(
|
||||
'Use exact model names such as gpt-4o, or regex rules prefixed with re: such as re:^gemini-.'
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
'Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.'
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
'Leave the final split empty as the fallback for models not matched above.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipIconButton({
|
||||
label,
|
||||
icon: Icon,
|
||||
disabled,
|
||||
onClick,
|
||||
}: {
|
||||
label: string
|
||||
icon: LucideIcon
|
||||
disabled?: boolean
|
||||
onClick: () => void
|
||||
}) {
|
||||
return (
|
||||
<TooltipProvider delay={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Icon data-icon='inline-start' aria-hidden='true' />
|
||||
<span className='sr-only'>{label}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='top'>{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function FieldBlock({
|
||||
label,
|
||||
className,
|
||||
labelClassName,
|
||||
children,
|
||||
}: {
|
||||
label: string
|
||||
label: ReactNode
|
||||
className?: string
|
||||
labelClassName?: string
|
||||
children: ReactNode
|
||||
|
||||
+196
-14
@@ -29,31 +29,47 @@ export const CHANNEL_TYPE_ADVANCED_CUSTOM = 58
|
||||
export const ADVANCED_CUSTOM_CONVERTER_OPTIONS: Array<{
|
||||
value: AdvancedCustomConverter
|
||||
label: string
|
||||
triggerLabel: string
|
||||
}> = [
|
||||
{ value: 'none', label: 'Native forwarding' },
|
||||
{
|
||||
value: 'none',
|
||||
label: 'Native forwarding',
|
||||
triggerLabel: 'Native forwarding',
|
||||
},
|
||||
{
|
||||
value: 'anthropic_messages_to_openai_chat_completions',
|
||||
label: 'Anthropic Messages to OpenAI Chat',
|
||||
triggerLabel: 'To OpenAI Chat',
|
||||
},
|
||||
{
|
||||
value: 'openai_chat_completions_to_anthropic_messages',
|
||||
label: 'OpenAI Chat to Anthropic Messages',
|
||||
triggerLabel: 'To Anthropic Messages',
|
||||
},
|
||||
{
|
||||
value: 'openai_chat_completions_to_openai_responses',
|
||||
label: 'OpenAI Chat to OpenAI Responses',
|
||||
triggerLabel: 'To OpenAI Responses',
|
||||
},
|
||||
{
|
||||
value: 'openai_responses_to_openai_chat_completions',
|
||||
label: 'OpenAI Responses to OpenAI Chat',
|
||||
triggerLabel: 'To OpenAI Chat',
|
||||
},
|
||||
{
|
||||
value: 'openai_responses_to_gemini_generate_content',
|
||||
label: 'OpenAI Responses to Gemini Generate Content',
|
||||
triggerLabel: 'To Gemini Generate Content',
|
||||
},
|
||||
{
|
||||
value: 'gemini_generate_content_to_openai_chat_completions',
|
||||
label: 'Gemini Generate Content to OpenAI Chat',
|
||||
triggerLabel: 'To OpenAI Chat',
|
||||
},
|
||||
{
|
||||
value: 'openai_chat_completions_to_gemini_generate_content',
|
||||
label: 'OpenAI Chat to Gemini Generate Content',
|
||||
triggerLabel: 'To Gemini Generate Content',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -157,6 +173,20 @@ export type AdvancedCustomTemplateOption = {
|
||||
config: AdvancedCustomConfig
|
||||
}
|
||||
|
||||
export type AdvancedCustomConverterDefaults = {
|
||||
upstream_path: string
|
||||
auth?: AdvancedCustomRouteAuth
|
||||
}
|
||||
|
||||
export const ADVANCED_CUSTOM_MODEL_REGEX_PREFIX = 're:'
|
||||
|
||||
export type AdvancedCustomModelRuleKind = 'exact' | 'regex'
|
||||
|
||||
const openAIChatPath = '/v1/chat/completions'
|
||||
const openAIResponsesPath = '/v1/responses'
|
||||
const claudeMessagesPath = '/v1/messages'
|
||||
const geminiGenerateContentPath = '/v1beta/models/{model}:generateContent'
|
||||
|
||||
const bearerHeaderAuth = (): AdvancedCustomRouteAuth => ({
|
||||
type: 'header',
|
||||
name: 'Authorization',
|
||||
@@ -313,8 +343,8 @@ export function getAdvancedCustomTemplateConfig(
|
||||
|
||||
export function createAdvancedCustomRoute(): AdvancedCustomRoute {
|
||||
return {
|
||||
incoming_path: '/v1/chat/completions',
|
||||
upstream_path: '/v1/chat/completions',
|
||||
incoming_path: openAIChatPath,
|
||||
upstream_path: openAIChatPath,
|
||||
converter: 'none',
|
||||
}
|
||||
}
|
||||
@@ -326,18 +356,67 @@ export function createAdvancedCustomConfig(): AdvancedCustomConfig {
|
||||
}
|
||||
|
||||
export function getAdvancedCustomUpstreamPathPlaceholder(
|
||||
converter: AdvancedCustomConverter
|
||||
converter: AdvancedCustomConverter,
|
||||
incomingPath = getDefaultAdvancedCustomIncomingPath(converter)
|
||||
): string {
|
||||
if (converter === 'openai_chat_completions_to_gemini_generate_content') {
|
||||
return '/v1beta/models/{model}:generateContent'
|
||||
return getAdvancedCustomConverterDefaults(converter, incomingPath)
|
||||
.upstream_path
|
||||
}
|
||||
|
||||
export function getAdvancedCustomConverterDefaults(
|
||||
converter: AdvancedCustomConverter,
|
||||
incomingPath: string
|
||||
): AdvancedCustomConverterDefaults {
|
||||
const normalizedIncomingPath =
|
||||
incomingPath.trim() || getDefaultAdvancedCustomIncomingPath(converter)
|
||||
|
||||
if (converter === 'none') {
|
||||
return {
|
||||
upstream_path: normalizedIncomingPath,
|
||||
auth: getAdvancedCustomNativeAuth(normalizedIncomingPath),
|
||||
}
|
||||
}
|
||||
if (
|
||||
converter === 'anthropic_messages_to_openai_chat_completions' ||
|
||||
converter === 'gemini_generate_content_to_openai_chat_completions' ||
|
||||
converter === 'openai_responses_to_openai_chat_completions'
|
||||
) {
|
||||
return { upstream_path: openAIChatPath, auth: bearerHeaderAuth() }
|
||||
}
|
||||
if (converter === 'openai_chat_completions_to_openai_responses') {
|
||||
return { upstream_path: openAIResponsesPath, auth: bearerHeaderAuth() }
|
||||
}
|
||||
if (converter === 'openai_chat_completions_to_anthropic_messages') {
|
||||
return '/v1/messages'
|
||||
return { upstream_path: claudeMessagesPath, auth: apiKeyHeaderAuth() }
|
||||
}
|
||||
if (converter === 'openai_responses_to_openai_chat_completions') {
|
||||
return '/v1/chat/completions'
|
||||
if (
|
||||
converter === 'openai_chat_completions_to_gemini_generate_content' ||
|
||||
converter === 'openai_responses_to_gemini_generate_content'
|
||||
) {
|
||||
return { upstream_path: geminiGenerateContentPath, auth: geminiQueryAuth() }
|
||||
}
|
||||
return '/v1/chat/completions'
|
||||
|
||||
return {
|
||||
upstream_path: normalizedIncomingPath || openAIChatPath,
|
||||
auth: getAdvancedCustomNativeAuth(normalizedIncomingPath),
|
||||
}
|
||||
}
|
||||
|
||||
function getAdvancedCustomNativeAuth(
|
||||
incomingPath: string
|
||||
): AdvancedCustomRouteAuth {
|
||||
if (incomingPath === claudeMessagesPath) {
|
||||
return apiKeyHeaderAuth()
|
||||
}
|
||||
if (
|
||||
incomingPath.includes(':generateContent') ||
|
||||
incomingPath.includes(':streamGenerateContent') ||
|
||||
incomingPath.includes(':embedContent') ||
|
||||
incomingPath.includes(':batchEmbedContents')
|
||||
) {
|
||||
return geminiQueryAuth()
|
||||
}
|
||||
return bearerHeaderAuth()
|
||||
}
|
||||
|
||||
export function getAdvancedCustomIncomingPathOptions(
|
||||
@@ -416,6 +495,29 @@ export function normalizeAdvancedCustomConfig(
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAdvancedCustomRouteModels(value: string): string[] {
|
||||
return [
|
||||
...new Set(
|
||||
value
|
||||
.split(',')
|
||||
.map((model) => model.trim())
|
||||
.filter(Boolean)
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
export function getAdvancedCustomModelRuleKind(
|
||||
modelRule: string
|
||||
): AdvancedCustomModelRuleKind {
|
||||
return modelRule.startsWith(ADVANCED_CUSTOM_MODEL_REGEX_PREFIX)
|
||||
? 'regex'
|
||||
: 'exact'
|
||||
}
|
||||
|
||||
export function getAdvancedCustomRegexModelPattern(modelRule: string): string {
|
||||
return modelRule.slice(ADVANCED_CUSTOM_MODEL_REGEX_PREFIX.length)
|
||||
}
|
||||
|
||||
export function validateAdvancedCustomConfig(
|
||||
config: AdvancedCustomConfig | null
|
||||
): AdvancedCustomValidationError | null {
|
||||
@@ -431,12 +533,16 @@ export function validateAdvancedCustomConfig(
|
||||
}
|
||||
}
|
||||
|
||||
const seenPaths = new Set<string>()
|
||||
const routeModelsByPath = new Map<
|
||||
string,
|
||||
{ catchAllIndex: number | null; models: Map<string, number> }
|
||||
>()
|
||||
for (let index = 0; index < routes.length; index += 1) {
|
||||
const route = routes[index]
|
||||
const incomingPath = route.incoming_path?.trim() || ''
|
||||
const upstreamPath = getAdvancedCustomRouteUpstreamPath(route)
|
||||
const converter = route.converter || 'none'
|
||||
const routeModels = normalizeAdvancedCustomRouteModels(route.models)
|
||||
|
||||
if (!incomingPath) {
|
||||
return { routeIndex: index, message: 'Incoming path is required' }
|
||||
@@ -450,10 +556,15 @@ export function validateAdvancedCustomConfig(
|
||||
message: 'Incoming path must not include query',
|
||||
}
|
||||
}
|
||||
if (seenPaths.has(incomingPath)) {
|
||||
return { routeIndex: index, message: 'Incoming path must be unique' }
|
||||
const routeModelsError = validateAdvancedCustomRouteModels(
|
||||
index,
|
||||
incomingPath,
|
||||
routeModels,
|
||||
routeModelsByPath
|
||||
)
|
||||
if (routeModelsError) {
|
||||
return routeModelsError
|
||||
}
|
||||
seenPaths.add(incomingPath)
|
||||
|
||||
if (!upstreamPath) {
|
||||
return { routeIndex: index, message: 'Upstream path is required' }
|
||||
@@ -555,6 +666,10 @@ function normalizeAdvancedCustomRoute(
|
||||
upstream_path: getAdvancedCustomRouteUpstreamPath(route),
|
||||
converter: route.converter || 'none',
|
||||
}
|
||||
const models = normalizeAdvancedCustomRouteModels(route.models)
|
||||
if (models.length > 0) {
|
||||
nextRoute.models = models
|
||||
}
|
||||
if (route.auth) {
|
||||
nextRoute.auth = {
|
||||
type: route.auth.type,
|
||||
@@ -565,6 +680,70 @@ function normalizeAdvancedCustomRoute(
|
||||
return nextRoute
|
||||
}
|
||||
|
||||
function normalizeAdvancedCustomRouteModels(
|
||||
models: string[] | undefined
|
||||
): string[] {
|
||||
if (!Array.isArray(models)) return []
|
||||
return models.map((model) => model.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
function validateAdvancedCustomRouteModels(
|
||||
routeIndex: number,
|
||||
incomingPath: string,
|
||||
models: string[],
|
||||
routeModelsByPath: Map<
|
||||
string,
|
||||
{ catchAllIndex: number | null; models: Map<string, number> }
|
||||
>
|
||||
): AdvancedCustomValidationError | null {
|
||||
let state = routeModelsByPath.get(incomingPath)
|
||||
if (!state) {
|
||||
state = { catchAllIndex: null, models: new Map<string, number>() }
|
||||
routeModelsByPath.set(incomingPath, state)
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
if (state.catchAllIndex !== null) {
|
||||
return {
|
||||
routeIndex,
|
||||
message:
|
||||
'Only one catch-all route is allowed for the same incoming path',
|
||||
}
|
||||
}
|
||||
state.catchAllIndex = routeIndex
|
||||
return null
|
||||
}
|
||||
|
||||
if (state.catchAllIndex !== null) {
|
||||
return {
|
||||
routeIndex,
|
||||
message: 'Catch-all route must be last for the same incoming path',
|
||||
}
|
||||
}
|
||||
|
||||
const seenInRoute = new Set<string>()
|
||||
for (const model of models) {
|
||||
if (
|
||||
getAdvancedCustomModelRuleKind(model) === 'regex' &&
|
||||
getAdvancedCustomRegexModelPattern(model) === ''
|
||||
) {
|
||||
return { routeIndex, message: 'Model regex cannot be empty' }
|
||||
}
|
||||
if (seenInRoute.has(model)) {
|
||||
return { routeIndex, message: 'Duplicate model in route models' }
|
||||
}
|
||||
seenInRoute.add(model)
|
||||
if (state.models.has(model)) {
|
||||
return {
|
||||
routeIndex,
|
||||
message: 'Route models must be unique for the same incoming path',
|
||||
}
|
||||
}
|
||||
state.models.set(model, routeIndex)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getAdvancedCustomRouteUpstreamPath(
|
||||
route: AdvancedCustomRoute
|
||||
): string {
|
||||
@@ -622,6 +801,9 @@ function isConverterPathAllowed(
|
||||
if (converter === 'openai_responses_to_openai_chat_completions') {
|
||||
return incomingPath === '/v1/responses'
|
||||
}
|
||||
if (converter === 'openai_responses_to_gemini_generate_content') {
|
||||
return incomingPath === '/v1/responses'
|
||||
}
|
||||
return (
|
||||
incomingPath.includes(':generateContent') ||
|
||||
incomingPath.includes(':streamGenerateContent')
|
||||
|
||||
+2
@@ -117,6 +117,7 @@ export interface AdvancedCustomRoute {
|
||||
incoming_path?: string
|
||||
upstream_path?: string
|
||||
converter?: AdvancedCustomConverter
|
||||
models?: string[]
|
||||
auth?: AdvancedCustomRouteAuth
|
||||
}
|
||||
|
||||
@@ -132,6 +133,7 @@ export type AdvancedCustomConverter =
|
||||
| 'openai_chat_completions_to_anthropic_messages'
|
||||
| 'openai_chat_completions_to_openai_responses'
|
||||
| 'openai_responses_to_openai_chat_completions'
|
||||
| 'openai_responses_to_gemini_generate_content'
|
||||
| 'gemini_generate_content_to_openai_chat_completions'
|
||||
| 'openai_chat_completions_to_gemini_generate_content'
|
||||
|
||||
|
||||
+42
-10
@@ -31,6 +31,7 @@ import {
|
||||
Info,
|
||||
LogIn,
|
||||
} from 'lucide-react'
|
||||
import type { TFunction } from 'i18next'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Dialog } from '@/components/dialog'
|
||||
@@ -62,7 +63,7 @@ import {
|
||||
isPerCallBilling,
|
||||
isTimingLogType,
|
||||
} from '../../lib/utils'
|
||||
import type { LogOtherData } from '../../types'
|
||||
import { USAGE_BILLING_PATH, type LogOtherData } from '../../types'
|
||||
|
||||
// Maps a channel-update changed-field token (as recorded by the backend audit)
|
||||
// to its i18n label key for display in the audit details.
|
||||
@@ -150,6 +151,41 @@ function formatRatio(ratio: number | undefined): string {
|
||||
return ratio.toFixed(4)
|
||||
}
|
||||
|
||||
function getUsageBillingPathLabel(
|
||||
t: TFunction,
|
||||
adminInfo: LogOtherData['admin_info']
|
||||
): string {
|
||||
switch (adminInfo?.usage_billing_path) {
|
||||
case USAGE_BILLING_PATH.LOCAL:
|
||||
return t('Local Billing')
|
||||
case USAGE_BILLING_PATH.OPENAI:
|
||||
return t('Upstream Response (billing-usage-openai)')
|
||||
case USAGE_BILLING_PATH.OPENAI_ESTIMATED:
|
||||
return t('Upstream Response (billing-usage-openai-estimated)')
|
||||
case USAGE_BILLING_PATH.ANTHROPIC:
|
||||
return t('Upstream Response (billing-usage-anthropic)')
|
||||
case USAGE_BILLING_PATH.ANTHROPIC_ESTIMATED:
|
||||
return t('Upstream Response (billing-usage-anthropic-estimated)')
|
||||
case USAGE_BILLING_PATH.GEMINI:
|
||||
return t('Upstream Response (billing-usage-gemini)')
|
||||
case USAGE_BILLING_PATH.GEMINI_ESTIMATED:
|
||||
return t('Upstream Response (billing-usage-gemini-estimated)')
|
||||
case USAGE_BILLING_PATH.UPSTREAM:
|
||||
return t('Upstream Response')
|
||||
default:
|
||||
return adminInfo?.local_count_tokens
|
||||
? t('Local Billing')
|
||||
: t('Upstream Response')
|
||||
}
|
||||
}
|
||||
|
||||
function isUsageBillingPathLocal(adminInfo: LogOtherData['admin_info']): boolean {
|
||||
if (adminInfo?.usage_billing_path) {
|
||||
return adminInfo.usage_billing_path === USAGE_BILLING_PATH.LOCAL
|
||||
}
|
||||
return adminInfo?.local_count_tokens === true
|
||||
}
|
||||
|
||||
function quotaSaturationKindLabel(
|
||||
kind: 'overflow' | 'underflow' | 'nan',
|
||||
t: (key: string) => string
|
||||
@@ -326,10 +362,8 @@ function BillingBreakdown(props: {
|
||||
|
||||
if (isAdmin && other.admin_info) {
|
||||
rows.push({
|
||||
label: t('Billing Source'),
|
||||
value: other.admin_info.local_count_tokens
|
||||
? t('Local Billing')
|
||||
: t('Upstream Response'),
|
||||
label: t('Billing Path'),
|
||||
value: getUsageBillingPathLabel(t, other.admin_info),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1037,18 +1071,16 @@ export function DetailsDialog(props: DetailsDialogProps) {
|
||||
props.log.type !== 6 &&
|
||||
other?.admin_info && (
|
||||
<DetailRow
|
||||
label={t('Billing Source')}
|
||||
label={t('Billing Path')}
|
||||
value={
|
||||
<span className='flex items-center gap-1'>
|
||||
{other.admin_info.local_count_tokens ? (
|
||||
{isUsageBillingPathLocal(other.admin_info) ? (
|
||||
<Monitor className='size-3 text-blue-500' />
|
||||
) : (
|
||||
<Cloud className='size-3 text-emerald-500' />
|
||||
)}
|
||||
<span className='text-xs'>
|
||||
{other.admin_info.local_count_tokens
|
||||
? t('Local Billing')
|
||||
: t('Upstream Response')}
|
||||
{getUsageBillingPathLabel(t, other.admin_info)}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
|
||||
+15
@@ -92,12 +92,27 @@ export interface ChannelAffinityInfo {
|
||||
using_group?: string
|
||||
}
|
||||
|
||||
export const USAGE_BILLING_PATH = {
|
||||
LOCAL: 'local',
|
||||
UPSTREAM: 'upstream',
|
||||
OPENAI: 'billing-usage-openai',
|
||||
OPENAI_ESTIMATED: 'billing-usage-openai-estimated',
|
||||
ANTHROPIC: 'billing-usage-anthropic',
|
||||
ANTHROPIC_ESTIMATED: 'billing-usage-anthropic-estimated',
|
||||
GEMINI: 'billing-usage-gemini',
|
||||
GEMINI_ESTIMATED: 'billing-usage-gemini-estimated',
|
||||
} as const
|
||||
|
||||
export type UsageBillingPath =
|
||||
(typeof USAGE_BILLING_PATH)[keyof typeof USAGE_BILLING_PATH]
|
||||
|
||||
export interface LogOtherData {
|
||||
admin_info?: {
|
||||
is_multi_key?: boolean
|
||||
multi_key_index?: number
|
||||
use_channel?: number[]
|
||||
local_count_tokens?: boolean
|
||||
usage_billing_path?: UsageBillingPath | string
|
||||
channel_affinity?: ChannelAffinityInfo
|
||||
// Top-up audit fields (type=1, admin only)
|
||||
payment_method?: string
|
||||
|
||||
Vendored
+54
@@ -207,6 +207,7 @@
|
||||
"Add rule group": "Add rule group",
|
||||
"Add rules for a user group": "Add rules for a user group",
|
||||
"Add selectable group": "Add selectable group",
|
||||
"Add split": "Add split",
|
||||
"Add subscription": "Add subscription",
|
||||
"Add tags...": "Add tags...",
|
||||
"Add tier": "Add tier",
|
||||
@@ -617,6 +618,7 @@
|
||||
"Billing group = vip (the token has no group, so use the user group)": "Billing group = vip (the token has no group, so use the user group)",
|
||||
"Billing History": "Billing History",
|
||||
"Billing Mode": "Billing Mode",
|
||||
"Billing Path": "Billing Path",
|
||||
"Billing Process": "Billing Process",
|
||||
"Billing rule: each call is billed as the token group (falling back to the user group when the token has none). The base ratio always comes from that billing group, not from the user group. To give a user group a special price on another billing group, add an entry in the override matrix.": "Billing rule: each call is billed as the token group (falling back to the user group when the token has none). The base ratio always comes from that billing group, not from the user group. To give a user group a special price on another billing group, add an entry in the override matrix.",
|
||||
"Billing Source": "Billing Source",
|
||||
@@ -715,6 +717,7 @@
|
||||
"Caps the response length": "Caps the response length",
|
||||
"Capture a reusable bundle of models, tags, or endpoints.": "Capture a reusable bundle of models, tags, or endpoints.",
|
||||
"Card view": "Card view",
|
||||
"Catch-all route must be last for the same incoming path": "Catch-all route must be last for the same incoming path",
|
||||
"Category": "Category",
|
||||
"Category Name": "Category Name",
|
||||
"Category name is required": "Category name is required",
|
||||
@@ -867,6 +870,9 @@
|
||||
"Click to view image": "Click to view image",
|
||||
"Client header value": "Client header value",
|
||||
"Client ID": "Client ID",
|
||||
"Client model": "Client model",
|
||||
"Client model matching": "Client model matching",
|
||||
"Client model matching help": "Client model matching help",
|
||||
"Client Secret": "Client Secret",
|
||||
"Close": "Close",
|
||||
"Close dialog": "Close dialog",
|
||||
@@ -1446,6 +1452,7 @@
|
||||
"Drawing task records": "Drawing task records",
|
||||
"Duplicate": "Duplicate",
|
||||
"Duplicate group names: {{names}}": "Duplicate group names: {{names}}",
|
||||
"Duplicate model in route models": "Duplicate model in route models",
|
||||
"Duplicate source model mappings are not allowed": "Duplicate source model mappings are not allowed",
|
||||
"Duplicate source model(s): {{models}}": "Duplicate source model(s): {{models}}",
|
||||
"Duration": "Duration",
|
||||
@@ -1460,6 +1467,7 @@
|
||||
"e.g. Basic Plan": "e.g. Basic Plan",
|
||||
"e.g. Clean tool parameters to avoid upstream validation errors": "e.g. Clean tool parameters to avoid upstream validation errors",
|
||||
"e.g. example.com": "e.g. example.com",
|
||||
"e.g. gpt-4o, gemini-2.5-flash": "e.g. gpt-4o, gemini-2.5-flash",
|
||||
"e.g. llama3.1:8b": "e.g. llama3.1:8b",
|
||||
"e.g. My GitLab": "e.g. My GitLab",
|
||||
"e.g. my-gitlab": "e.g. my-gitlab",
|
||||
@@ -1717,6 +1725,7 @@
|
||||
"Everything configured for this group, in one place.": "Everything configured for this group, in one place.",
|
||||
"Exact": "Exact",
|
||||
"Exact Match": "Exact Match",
|
||||
"Exact match only and case-sensitive. Prefixes, regex, and * wildcards are not supported.": "Exact match only and case-sensitive. Prefixes, regex, and * wildcards are not supported.",
|
||||
"Example": "Example",
|
||||
"Example (all channels):": "Example (all channels):",
|
||||
"Example (specific channels):": "Example (specific channels):",
|
||||
@@ -1903,7 +1912,11 @@
|
||||
"Failed to update user": "Failed to update user",
|
||||
"Failure keywords": "Failure keywords",
|
||||
"Fair": "Fair",
|
||||
"Fallback": "Fallback",
|
||||
"Fallback base URL": "Fallback base URL",
|
||||
"Fallback for remaining models": "Fallback for remaining models",
|
||||
"Fallback must be last": "Fallback must be last",
|
||||
"Fallback route": "Fallback route",
|
||||
"Fallback tier": "Fallback tier",
|
||||
"FAQ": "FAQ",
|
||||
"FAQ added. Click \"Save Settings\" to apply.": "FAQ added. Click \"Save Settings\" to apply.",
|
||||
@@ -1941,6 +1954,7 @@
|
||||
"Fill Related Models": "Fill Related Models",
|
||||
"Fill Template": "Fill Template",
|
||||
"Fill Templates": "Fill Templates",
|
||||
"Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.",
|
||||
"Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format",
|
||||
"Filled {{count}} model(s)": "Filled {{count}} model(s)",
|
||||
"Filled {{count}} related model(s)": "Filled {{count}} related model(s)",
|
||||
@@ -1982,6 +1996,7 @@
|
||||
"First token": "First token",
|
||||
"First/Last Frame to Video": "First/Last Frame to Video",
|
||||
"Fix Abilities": "Repair Channel Consistency",
|
||||
"Fix order": "Fix order",
|
||||
"Fixed abilities: {{success}} succeeded, {{fails}} failed": "Channel consistency repaired: {{success}} succeeded, {{fails}} failed",
|
||||
"Fixed price": "Fixed price",
|
||||
"Fixed price (USD)": "Fixed price (USD)",
|
||||
@@ -2426,16 +2441,21 @@
|
||||
"Leave blank to keep the existing credential": "Leave blank to keep the existing credential",
|
||||
"Leave blank to keep the existing key": "Leave blank to keep the existing key",
|
||||
"Leave blank unless rotating the secret": "Leave blank unless rotating the secret",
|
||||
"Leave empty for fallback": "Leave empty for fallback",
|
||||
"Leave empty for never expires": "Leave empty for never expires",
|
||||
"Leave empty only for the final fallback split.": "Leave empty only for the final fallback split.",
|
||||
"Leave empty to disable the agreement requirement. Supports Markdown, HTML, or a full URL to redirect users.": "Leave empty to disable the agreement requirement. Supports Markdown, HTML, or a full URL to redirect users.",
|
||||
"Leave empty to disable the privacy policy requirement. Supports Markdown, HTML, or a full URL to redirect users.": "Leave empty to disable the privacy policy requirement. Supports Markdown, HTML, or a full URL to redirect users.",
|
||||
"Leave empty to disband the tag": "Leave empty to disband the tag",
|
||||
"Leave empty to keep existing key": "Leave empty to keep existing key",
|
||||
"Leave empty to keep unchanged": "Leave empty to keep unchanged",
|
||||
"Leave empty to match all models": "Leave empty to match all models",
|
||||
"Leave empty to use account email": "Leave empty to use account email",
|
||||
"Leave empty to use default": "Leave empty to use default",
|
||||
"Leave empty to use system temp directory": "Leave empty to use system temp directory",
|
||||
"Leave empty to use username": "Leave empty to use username",
|
||||
"Leave the final split empty as the fallback for models not matched above.": "Leave the final split empty as the fallback for models not matched above.",
|
||||
"Leave this empty only for the final fallback split; it catches client models not matched above.": "Leave this empty only for the final fallback split; it catches client models not matched above.",
|
||||
"Left to Right": "Left to Right",
|
||||
"Legacy Format (JSON Object)": "Legacy Format (JSON Object)",
|
||||
"Legacy format must be a JSON object": "Legacy format must be a JSON object",
|
||||
@@ -2482,6 +2502,9 @@
|
||||
"Loading...": "Loading...",
|
||||
"Local": "Local",
|
||||
"Local Billing": "Local Billing",
|
||||
"Local Estimate (billing-usage-anthropic)": "Local Estimate (billing-usage-anthropic)",
|
||||
"Local Estimate (billing-usage-gemini)": "Local Estimate (billing-usage-gemini)",
|
||||
"Local Estimate (billing-usage-openai)": "Local Estimate (billing-usage-openai)",
|
||||
"Local models": "Local models",
|
||||
"Locations": "Locations",
|
||||
"Locked": "Locked",
|
||||
@@ -2548,7 +2571,9 @@
|
||||
"Match Value": "Match Value",
|
||||
"Match Value (optional)": "Match Value (optional)",
|
||||
"Matched": "Matched",
|
||||
"Matched models": "Matched models",
|
||||
"Matched Tier": "Matched Tier",
|
||||
"Matches models not claimed by earlier splits.": "Matches models not claimed by earlier splits.",
|
||||
"Matching Rules": "Matching Rules",
|
||||
"Max Disk Cache Size (MB)": "Max Disk Cache Size (MB)",
|
||||
"Max Entries": "Max Entries",
|
||||
@@ -2662,12 +2687,15 @@
|
||||
"Model ratios reset successfully": "Model ratios reset successfully",
|
||||
"Model Regex": "Model Regex",
|
||||
"Model Regex (one per line)": "Model Regex (one per line)",
|
||||
"Model regex cannot be empty": "Model regex cannot be empty",
|
||||
"Model scope": "Model scope",
|
||||
"Model selected": "Model selected",
|
||||
"Model Square": "Model Square",
|
||||
"Model Tags": "Model Tags",
|
||||
"Model to use for testing": "Model to use for testing",
|
||||
"Model to use when testing channel connectivity": "Model to use when testing channel connectivity",
|
||||
"Model Version *": "Model Version *",
|
||||
"Model-scoped only": "Model-scoped only",
|
||||
"model(s) selected out of": "model(s) selected out of",
|
||||
"model(s)? This action cannot be undone.": "model(s)? This action cannot be undone.",
|
||||
"models": "models",
|
||||
@@ -2714,9 +2742,12 @@
|
||||
"Move": "Move",
|
||||
"Move a request header": "Move a request header",
|
||||
"Move affiliate rewards to your main balance": "Move affiliate rewards to your main balance",
|
||||
"Move fallback to end": "Move fallback to end",
|
||||
"Move Field": "Move Field",
|
||||
"Move Header": "Move Header",
|
||||
"Move Request Header": "Move Request Header",
|
||||
"Move route down": "Move route down",
|
||||
"Move route up": "Move route up",
|
||||
"Move source field to target field": "Move source field to target field",
|
||||
"ms": "ms",
|
||||
"Multi-key channel: Keys will be": "Multi-key channel: Keys will be",
|
||||
@@ -3050,6 +3081,7 @@
|
||||
"Only enabled parameters are sent with the request.": "Only enabled parameters are sent with the request.",
|
||||
"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 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.",
|
||||
@@ -3088,6 +3120,7 @@
|
||||
"OpenAI Rerank": "OpenAI Rerank",
|
||||
"OpenAI Responses": "OpenAI Responses",
|
||||
"OpenAI Responses Compact": "OpenAI Responses Compact",
|
||||
"OpenAI Responses to Gemini Generate Content": "OpenAI Responses to Gemini Generate Content",
|
||||
"OpenAI Responses to OpenAI Chat": "OpenAI Responses to OpenAI Chat",
|
||||
"OpenAI, Anthropic, etc.": "OpenAI, Anthropic, etc.",
|
||||
"OpenAI, Anthropic, Google, etc.": "OpenAI, Anthropic, Google, etc.",
|
||||
@@ -3835,9 +3868,15 @@
|
||||
"Route": "Route",
|
||||
"Route active": "Route active",
|
||||
"Route Description": "Route Description",
|
||||
"Route group": "Route group",
|
||||
"Route is required": "Route is required",
|
||||
"Route models must be unique for the same incoming path": "Route models must be unique for the same incoming path",
|
||||
"Route, auth, and balance check in one place": "Route, auth, and balance check in one place",
|
||||
"Routes": "Routes",
|
||||
"Routes with the same incoming path are matched by model. Leave the model scope empty only for the final fallback route.": "Routes with the same incoming path are matched by model. Leave the model scope empty only for the final fallback route.",
|
||||
"Routes with the same incoming path are split by client model rules. Unmatched requests use the final fallback.": "Routes with the same incoming path are split by client model rules. Unmatched requests use the final fallback.",
|
||||
"Routes with the same incoming path are split by exact client model. Unmatched requests use the final fallback.": "Routes with the same incoming path are split by exact client model. Unmatched requests use the final fallback.",
|
||||
"Routes with the same incoming path match exact client model names. Separate multiple models with commas, and leave only the final fallback empty.": "Routes with the same incoming path match exact client model names. Separate multiple models with commas, and leave only the final fallback empty.",
|
||||
"Routing & Overrides": "Routing & Overrides",
|
||||
"Routing Reliability": "Routing Reliability",
|
||||
"Routing Strategy": "Routing Strategy",
|
||||
@@ -3862,6 +3901,7 @@
|
||||
"Rules": "Rules",
|
||||
"Rules JSON": "Rules JSON",
|
||||
"Rules JSON must be an array": "Rules JSON must be an array",
|
||||
"Rules match the original model value from the client request body.": "Rules match the original model value from the client request body.",
|
||||
"Run GC": "Run GC",
|
||||
"Run tests for the selected models": "Run tests for the selected models",
|
||||
"running": "running",
|
||||
@@ -4057,6 +4097,7 @@
|
||||
"Sensitive Words": "Sensitive Words",
|
||||
"Sent the API key to FluentRead.": "Sent the API key to FluentRead.",
|
||||
"Separate image/audio prices are enabled.": "Separate image/audio prices are enabled.",
|
||||
"Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.",
|
||||
"Serve multiple users or teams with billing and quota control.": "Serve multiple users or teams with billing and quota control.",
|
||||
"Server Address": "Server Address",
|
||||
"Server IP": "Server IP",
|
||||
@@ -4470,6 +4511,7 @@
|
||||
"This FAQ entry will be removed from the list.": "This FAQ entry will be removed from the list.",
|
||||
"This feature is experimental. Configuration format and behavior may change.": "This feature is experimental. Configuration format and behavior may change.",
|
||||
"This feature requires server-side WeChat configuration": "This feature requires server-side WeChat configuration",
|
||||
"This field does not support wildcards or regular expressions.": "This field does not support wildcards or regular expressions.",
|
||||
"This historical record predates audit-info tracking and cannot be backfilled. The current instance already records server IP, callback IP, payment method, and system version for new top-ups going forward.": "This historical record predates audit-info tracking and cannot be backfilled. The current instance already records server IP, callback IP, payment method, and system version for new top-ups going forward.",
|
||||
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.",
|
||||
"This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.": "This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.",
|
||||
@@ -4536,9 +4578,13 @@
|
||||
"Timing": "Timing",
|
||||
"Tip": "Tip",
|
||||
"to access this resource.": "to access this resource.",
|
||||
"To Anthropic Messages": "To Anthropic Messages",
|
||||
"to confirm": "to confirm",
|
||||
"To Gemini Generate Content": "To Gemini Generate Content",
|
||||
"To Lower": "To Lower",
|
||||
"To Lowercase": "To Lowercase",
|
||||
"To OpenAI Chat": "To OpenAI Chat",
|
||||
"To OpenAI Responses": "To OpenAI Responses",
|
||||
"to override billing when a user in one group uses a token of another group.": "to override billing when a user in one group uses a token of another group.",
|
||||
"to the Models list so users can use them before the mapping sends traffic upstream.": "to the Models list so users can use them before the mapping sends traffic upstream.",
|
||||
"To Upper": "To Upper",
|
||||
@@ -4786,6 +4832,12 @@
|
||||
"Upstream ratios fetched successfully": "Upstream ratios fetched successfully",
|
||||
"Upstream Request ID": "Upstream Request ID",
|
||||
"Upstream Response": "Upstream Response",
|
||||
"Upstream Response (billing-usage-anthropic-estimated)": "Upstream Response (billing-usage-anthropic-estimated)",
|
||||
"Upstream Response (billing-usage-anthropic)": "Upstream Response (billing-usage-anthropic)",
|
||||
"Upstream Response (billing-usage-gemini-estimated)": "Upstream Response (billing-usage-gemini-estimated)",
|
||||
"Upstream Response (billing-usage-gemini)": "Upstream Response (billing-usage-gemini)",
|
||||
"Upstream Response (billing-usage-openai-estimated)": "Upstream Response (billing-usage-openai-estimated)",
|
||||
"Upstream Response (billing-usage-openai)": "Upstream Response (billing-usage-openai)",
|
||||
"upstream services integrated": "upstream services integrated",
|
||||
"Upstream Updates": "Upstream Updates",
|
||||
"Upstream URL": "Upstream URL",
|
||||
@@ -4819,6 +4871,8 @@
|
||||
"Use authenticator code": "Use authenticator code",
|
||||
"Use backup code": "Use backup code",
|
||||
"Use disk cache when request body exceeds this size": "Use disk cache when request body exceeds this size",
|
||||
"Use exact client model names, separated by commas. Prefixes and wildcards are not supported.": "Use exact client model names, separated by commas. Prefixes and wildcards are not supported.",
|
||||
"Use exact model names such as gpt-4o, or regex rules prefixed with re: such as re:^gemini-.": "Use exact model names such as gpt-4o, or regex rules prefixed with re: such as re:^gemini-.",
|
||||
"Use external tools to extend capabilities": "Use external tools to extend capabilities",
|
||||
"Use one available reset credit for this channel. The reset request is sent only after confirmation.": "Use one available reset credit for this channel. The reset request is sent only after confirmation.",
|
||||
"Use one available reset credit to refresh the current Codex usage windows.": "Use one available reset credit to refresh the current Codex usage windows.",
|
||||
|
||||
Vendored
+54
@@ -207,6 +207,7 @@
|
||||
"Add rule group": "Ajouter un groupe de règles",
|
||||
"Add rules for a user group": "Ajouter des règles pour un groupe d’utilisateurs",
|
||||
"Add selectable group": "Ajouter un groupe sélectionnable",
|
||||
"Add split": "Ajouter une branche",
|
||||
"Add subscription": "Ajouter un abonnement",
|
||||
"Add tags...": "Ajouter des étiquettes...",
|
||||
"Add tier": "Ajouter un palier",
|
||||
@@ -617,6 +618,7 @@
|
||||
"Billing group = vip (the token has no group, so use the user group)": "Groupe de facturation = vip (le jeton n’a pas de groupe, on utilise le groupe de l’utilisateur)",
|
||||
"Billing History": "Historique de facturation",
|
||||
"Billing Mode": "Mode de facturation",
|
||||
"Billing Path": "Chemin de facturation",
|
||||
"Billing Process": "Processus de facturation",
|
||||
"Billing rule: each call is billed as the token group (falling back to the user group when the token has none). The base ratio always comes from that billing group, not from the user group. To give a user group a special price on another billing group, add an entry in the override matrix.": "Règle de facturation : chaque appel est facturé selon le groupe du jeton (à défaut, le groupe de l’utilisateur). Le taux de base provient toujours de ce groupe de facturation, pas du groupe de l’utilisateur. Pour accorder à un groupe d’utilisateurs un tarif spécial sur un autre groupe de facturation, ajoutez une entrée dans la matrice de remplacement.",
|
||||
"Billing Source": "Source de facturation",
|
||||
@@ -715,6 +717,7 @@
|
||||
"Caps the response length": "Limite la longueur de la réponse",
|
||||
"Capture a reusable bundle of models, tags, or endpoints.": "Capturez un ensemble réutilisable de modèles, d'étiquettes ou de points de terminaison.",
|
||||
"Card view": "Vue cartes",
|
||||
"Catch-all route must be last for the same incoming path": "Le routage de secours doit être le dernier pour le même chemin d'entrée",
|
||||
"Category": "Catégorie",
|
||||
"Category Name": "Nom de la catégorie",
|
||||
"Category name is required": "Le nom de la catégorie est requis",
|
||||
@@ -867,6 +870,9 @@
|
||||
"Click to view image": "Cliquer pour voir l’image",
|
||||
"Client header value": "Valeur d'en-tête client",
|
||||
"Client ID": "ID client",
|
||||
"Client model": "Modèle client",
|
||||
"Client model matching": "Correspondance du model client",
|
||||
"Client model matching help": "Aide sur la correspondance du model client",
|
||||
"Client Secret": "Secret client",
|
||||
"Close": "Fermer",
|
||||
"Close dialog": "Fermer la boîte de dialogue",
|
||||
@@ -1446,6 +1452,7 @@
|
||||
"Drawing task records": "Historique des tâches de dessin",
|
||||
"Duplicate": "Dupliquer",
|
||||
"Duplicate group names: {{names}}": "Noms de groupe en double : {{names}}",
|
||||
"Duplicate model in route models": "Modèle dupliqué dans les modèles de route",
|
||||
"Duplicate source model mappings are not allowed": "Les mappages de modèles source en double ne sont pas autorisés",
|
||||
"Duplicate source model(s): {{models}}": "Modèle(s) source en double : {{models}}",
|
||||
"Duration": "Durée",
|
||||
@@ -1460,6 +1467,7 @@
|
||||
"e.g. Basic Plan": "ex. Plan de base",
|
||||
"e.g. Clean tool parameters to avoid upstream validation errors": "ex. Nettoyer les paramètres d'outils pour éviter les erreurs de validation en amont",
|
||||
"e.g. example.com": "par ex. example.com",
|
||||
"e.g. gpt-4o, gemini-2.5-flash": "p. ex. gpt-4o, gemini-2.5-flash",
|
||||
"e.g. llama3.1:8b": "p. ex. llama3.1:8b",
|
||||
"e.g. My GitLab": "par ex. Mon GitLab",
|
||||
"e.g. my-gitlab": "par ex. mon-gitlab",
|
||||
@@ -1717,6 +1725,7 @@
|
||||
"Everything configured for this group, in one place.": "Toute la configuration de ce groupe, au même endroit.",
|
||||
"Exact": "Exact",
|
||||
"Exact Match": "Correspondance exacte",
|
||||
"Exact match only and case-sensitive. Prefixes, regex, and * wildcards are not supported.": "Correspondance exacte uniquement, sensible à la casse. Les préfixes, regex et jokers * ne sont pas pris en charge.",
|
||||
"Example": "Exemple",
|
||||
"Example (all channels):": "Exemple (tous les canaux) :",
|
||||
"Example (specific channels):": "Exemple (canaux spécifiques) :",
|
||||
@@ -1903,7 +1912,11 @@
|
||||
"Failed to update user": "Échec de la mise à jour de l'utilisateur",
|
||||
"Failure keywords": "Mots-clés d'échec",
|
||||
"Fair": "Correct",
|
||||
"Fallback": "Repli",
|
||||
"Fallback base URL": "Base URL de fallback",
|
||||
"Fallback for remaining models": "Repli pour les modèles restants",
|
||||
"Fallback must be last": "Repli en dernier",
|
||||
"Fallback route": "Route de repli",
|
||||
"Fallback tier": "Palier de repli",
|
||||
"FAQ": "FAQ",
|
||||
"FAQ added. Click \"Save Settings\" to apply.": "FAQ ajouté. Cliquez sur \"Enregistrer les paramètres\" pour appliquer.",
|
||||
@@ -1941,6 +1954,7 @@
|
||||
"Fill Related Models": "Remplir les modèles associés",
|
||||
"Fill Template": "Remplir le modèle",
|
||||
"Fill Templates": "Remplir les modèles",
|
||||
"Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "Saisissez la valeur model complète du corps de requête client, par exemple gpt-4o ou gemini-2.5-flash. Séparez plusieurs modèles par des virgules.",
|
||||
"Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "Remplit thoughtSignature uniquement pour les canaux Gemini/Vertex utilisant le format OpenAI",
|
||||
"Filled {{count}} model(s)": "{{count}} modèle(s) rempli(s)",
|
||||
"Filled {{count}} related model(s)": "{{count}} modèle(s) associé(s) rempli(s)",
|
||||
@@ -1982,6 +1996,7 @@
|
||||
"First token": "1er token",
|
||||
"First/Last Frame to Video": "Première/Dernière image vers vidéo",
|
||||
"Fix Abilities": "Réparer la cohérence des canaux",
|
||||
"Fix order": "Corriger l’ordre",
|
||||
"Fixed abilities: {{success}} succeeded, {{fails}} failed": "Cohérence des canaux réparée : {{success}} réussie(s), {{fails}} échouée(s)",
|
||||
"Fixed price": "Prix fixe",
|
||||
"Fixed price (USD)": "Prix fixe (USD)",
|
||||
@@ -2426,16 +2441,21 @@
|
||||
"Leave blank to keep the existing credential": "Laissez vide pour conserver l'identifiant existant",
|
||||
"Leave blank to keep the existing key": "Laisser vide pour conserver la clé existante",
|
||||
"Leave blank unless rotating the secret": "Laissez vide, sauf si vous faites pivoter le secret",
|
||||
"Leave empty for fallback": "Laisser vide pour le repli",
|
||||
"Leave empty for never expires": "Laissez vide pour qu'il n'expire jamais",
|
||||
"Leave empty only for the final fallback split.": "Laissez vide uniquement pour la dernière branche de repli.",
|
||||
"Leave empty to disable the agreement requirement. Supports Markdown, HTML, or a full URL to redirect users.": "Laissez vide pour désactiver l'exigence d'accord. Prend en charge Markdown, HTML ou une URL complète pour rediriger les utilisateurs.",
|
||||
"Leave empty to disable the privacy policy requirement. Supports Markdown, HTML, or a full URL to redirect users.": "Laissez vide pour désactiver l'exigence de politique de confidentialité. Prend en charge Markdown, HTML ou une URL complète pour rediriger les utilisateurs.",
|
||||
"Leave empty to disband the tag": "Laissez vide pour dissoudre l'étiquette",
|
||||
"Leave empty to keep existing key": "Laissez vide pour conserver la clé existante",
|
||||
"Leave empty to keep unchanged": "Laissez vide pour conserver inchangé",
|
||||
"Leave empty to match all models": "Laisser vide pour correspondre à tous les modèles",
|
||||
"Leave empty to use account email": "Laissez vide pour utiliser l'e-mail du compte",
|
||||
"Leave empty to use default": "Laisser vide pour utiliser la valeur par défaut",
|
||||
"Leave empty to use system temp directory": "Laisser vide pour utiliser le répertoire temporaire",
|
||||
"Leave empty to use username": "Laissez vide pour utiliser le nom d'utilisateur",
|
||||
"Leave the final split empty as the fallback for models not matched above.": "Laissez la dernière répartition vide comme solution de secours pour les modèles non appariés plus haut.",
|
||||
"Leave this empty only for the final fallback split; it catches client models not matched above.": "Laissez vide uniquement pour la dernière branche de repli ; elle reçoit les modèles client non associés plus haut.",
|
||||
"Left to Right": "De gauche à droite",
|
||||
"Legacy Format (JSON Object)": "Ancien format (objet JSON)",
|
||||
"Legacy format must be a JSON object": "L'ancien format doit être un objet JSON",
|
||||
@@ -2482,6 +2502,9 @@
|
||||
"Loading...": "Chargement...",
|
||||
"Local": "Local",
|
||||
"Local Billing": "Facturation locale",
|
||||
"Local Estimate (billing-usage-anthropic)": "Estimation locale (billing-usage-anthropic)",
|
||||
"Local Estimate (billing-usage-gemini)": "Estimation locale (billing-usage-gemini)",
|
||||
"Local Estimate (billing-usage-openai)": "Estimation locale (billing-usage-openai)",
|
||||
"Local models": "Modèles locaux",
|
||||
"Locations": "Emplacements",
|
||||
"Locked": "Verrouillé",
|
||||
@@ -2548,7 +2571,9 @@
|
||||
"Match Value": "Valeur de correspondance",
|
||||
"Match Value (optional)": "Valeur de correspondance (optionnel)",
|
||||
"Matched": "Correspondant",
|
||||
"Matched models": "Modèles associés",
|
||||
"Matched Tier": "Palier correspondant",
|
||||
"Matches models not claimed by earlier splits.": "Correspond aux modèles non pris par les branches précédentes.",
|
||||
"Matching Rules": "Règles de correspondance",
|
||||
"Max Disk Cache Size (MB)": "Taille max du cache disque (Mo)",
|
||||
"Max Entries": "Entrées max",
|
||||
@@ -2662,12 +2687,15 @@
|
||||
"Model ratios reset successfully": "Ratios des modèles réinitialisés avec succès",
|
||||
"Model Regex": "Regex du modèle",
|
||||
"Model Regex (one per line)": "Regex du modèle (un par ligne)",
|
||||
"Model regex cannot be empty": "La regex du modèle ne peut pas être vide",
|
||||
"Model scope": "Portée des modèles",
|
||||
"Model selected": "Modèle sélectionné",
|
||||
"Model Square": "Place des modèles",
|
||||
"Model Tags": "Tags de modèle",
|
||||
"Model to use for testing": "Modèle à utiliser pour les tests",
|
||||
"Model to use when testing channel connectivity": "Modèle à utiliser lors du test de la connectivité du canal",
|
||||
"Model Version *": "Version du modèle *",
|
||||
"Model-scoped only": "Modèles uniquement",
|
||||
"model(s) selected out of": "modèle(s) sélectionné(s) parmi",
|
||||
"model(s)? This action cannot be undone.": "modèle(s) ? Cette action ne peut pas être annulée.",
|
||||
"models": "modèles",
|
||||
@@ -2714,9 +2742,12 @@
|
||||
"Move": "Déplacer",
|
||||
"Move a request header": "Déplacer un en-tête de requête",
|
||||
"Move affiliate rewards to your main balance": "Transférer les récompenses d'affiliation vers votre solde principal",
|
||||
"Move fallback to end": "Mettre le repli à la fin",
|
||||
"Move Field": "Déplacer le champ",
|
||||
"Move Header": "Déplacer l'en-tête",
|
||||
"Move Request Header": "Déplacer un en-tête de requête",
|
||||
"Move route down": "Descendre la route",
|
||||
"Move route up": "Monter la route",
|
||||
"Move source field to target field": "Déplacer le champ source vers le champ cible",
|
||||
"ms": "ms",
|
||||
"Multi-key channel: Keys will be": "Canal multi-clés : Les clés seront",
|
||||
@@ -3050,6 +3081,7 @@
|
||||
"Only enabled parameters are sent with the request.": "Seuls les paramètres activés sont envoyés avec la requête.",
|
||||
"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 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.",
|
||||
@@ -3088,6 +3120,7 @@
|
||||
"OpenAI Rerank": "OpenAI Rerank",
|
||||
"OpenAI Responses": "OpenAI Responses",
|
||||
"OpenAI Responses Compact": "OpenAI Responses Compact",
|
||||
"OpenAI Responses to Gemini Generate Content": "OpenAI Responses vers Gemini Generate Content",
|
||||
"OpenAI Responses to OpenAI Chat": "OpenAI Responses vers OpenAI Chat",
|
||||
"OpenAI, Anthropic, etc.": "OpenAI, Anthropic, etc.",
|
||||
"OpenAI, Anthropic, Google, etc.": "OpenAI, Anthropic, Google, etc.",
|
||||
@@ -3835,9 +3868,15 @@
|
||||
"Route": "Route",
|
||||
"Route active": "Route active",
|
||||
"Route Description": "Description de la route",
|
||||
"Route group": "Groupe de routes",
|
||||
"Route is required": "La route est requise",
|
||||
"Route models must be unique for the same incoming path": "Les modèles de route doivent être uniques pour le même chemin d'entrée",
|
||||
"Route, auth, and balance check in one place": "Routage, authentification et solde au même endroit",
|
||||
"Routes": "Routes",
|
||||
"Routes with the same incoming path are matched by model. Leave the model scope empty only for the final fallback route.": "Les routes avec le même chemin d’entrée sont associées par modèle. Laissez la portée de modèles vide uniquement pour la route de repli finale.",
|
||||
"Routes with the same incoming path are split by client model rules. Unmatched requests use the final fallback.": "Les routes avec le même chemin entrant sont réparties selon les règles du model client. Les requêtes non appariées utilisent la dernière route de secours.",
|
||||
"Routes with the same incoming path are split by exact client model. Unmatched requests use the final fallback.": "Les routes avec le même chemin d’entrée sont réparties par modèle client exact. Les requêtes non associées utilisent le repli final.",
|
||||
"Routes with the same incoming path match exact client model names. Separate multiple models with commas, and leave only the final fallback empty.": "Les routes avec le même chemin d’entrée correspondent aux noms exacts des modèles client. Séparez plusieurs modèles par des virgules et laissez vide uniquement le repli final.",
|
||||
"Routing & Overrides": "Routage et surcharges",
|
||||
"Routing Reliability": "Fiabilité du routage",
|
||||
"Routing Strategy": "Stratégie de routage",
|
||||
@@ -3862,6 +3901,7 @@
|
||||
"Rules": "Règles",
|
||||
"Rules JSON": "Règles JSON",
|
||||
"Rules JSON must be an array": "Le JSON des règles doit être un tableau",
|
||||
"Rules match the original model value from the client request body.": "Les règles correspondent à la valeur model originale du corps de la requête client.",
|
||||
"Run GC": "Exécuter le GC",
|
||||
"Run tests for the selected models": "Exécuter les tests pour les modèles sélectionnés",
|
||||
"running": "en cours",
|
||||
@@ -4057,6 +4097,7 @@
|
||||
"Sensitive Words": "Mots sensibles",
|
||||
"Sent the API key to FluentRead.": "Clé API envoyée à FluentRead.",
|
||||
"Separate image/audio prices are enabled.": "Les prix séparés pour l’image et l’audio sont activés.",
|
||||
"Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "Séparez plusieurs règles par des virgules anglaises. Pour les regex nécessitant des virgules, passez en JSON Text.",
|
||||
"Serve multiple users or teams with billing and quota control.": "Servir plusieurs utilisateurs ou équipes avec gestion de la facturation et des quotas.",
|
||||
"Server Address": "Adresse du serveur",
|
||||
"Server IP": "IP du serveur",
|
||||
@@ -4470,6 +4511,7 @@
|
||||
"This FAQ entry will be removed from the list.": "Cette entrée de FAQ sera retirée de la liste.",
|
||||
"This feature is experimental. Configuration format and behavior may change.": "Cette fonctionnalité est expérimentale. Le format de configuration et le comportement peuvent changer.",
|
||||
"This feature requires server-side WeChat configuration": "Cette fonctionnalité nécessite une configuration WeChat côté serveur",
|
||||
"This field does not support wildcards or regular expressions.": "Ce champ ne prend pas en charge les jokers ni les expressions régulières.",
|
||||
"This historical record predates audit-info tracking and cannot be backfilled. The current instance already records server IP, callback IP, payment method, and system version for new top-ups going forward.": "Cet enregistrement historique date d'avant le suivi des informations d'audit et ne peut pas être complété rétroactivement. La version actuelle enregistre déjà l'IP du serveur, l'IP de rappel, le mode de paiement et la version du système pour les nouveaux paiements à venir.",
|
||||
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "Cet identifiant est envoyé au backend de paiement lors de la création d’une commande. Utilisez alipay pour Alipay, wxpay pour WeChat Pay, stripe pour Stripe. Les valeurs personnalisées doivent être prises en charge par votre fournisseur de paiement.",
|
||||
"This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.": "Cette instance utilise un nom d’hôte automatique. Définissez NODE_NAME sur une valeur stable et unique pour la gestion multi-instance.",
|
||||
@@ -4536,9 +4578,13 @@
|
||||
"Timing": "Durée",
|
||||
"Tip": "Astuce",
|
||||
"to access this resource.": "pour accéder à cette ressource.",
|
||||
"To Anthropic Messages": "Vers Anthropic Messages",
|
||||
"to confirm": "pour confirmer",
|
||||
"To Gemini Generate Content": "Vers Gemini Generate Content",
|
||||
"To Lower": "En minuscules",
|
||||
"To Lowercase": "En minuscules",
|
||||
"To OpenAI Chat": "Vers OpenAI Chat",
|
||||
"To OpenAI Responses": "Vers OpenAI Responses",
|
||||
"to override billing when a user in one group uses a token of another group.": "pour remplacer la facturation lorsqu'un utilisateur d'un groupe utilise un jeton d'un autre groupe.",
|
||||
"to the Models list so users can use them before the mapping sends traffic upstream.": "à la liste des modèles afin que les utilisateurs puissent les utiliser avant que le mappage n'envoie le trafic en amont.",
|
||||
"To Upper": "En majuscules",
|
||||
@@ -4786,6 +4832,12 @@
|
||||
"Upstream ratios fetched successfully": "Ratios en amont récupérés avec succès",
|
||||
"Upstream Request ID": "ID de requête en amont",
|
||||
"Upstream Response": "Réponse amont",
|
||||
"Upstream Response (billing-usage-anthropic-estimated)": "Réponse amont (billing-usage-anthropic-estimated)",
|
||||
"Upstream Response (billing-usage-anthropic)": "Réponse amont (billing-usage-anthropic)",
|
||||
"Upstream Response (billing-usage-gemini-estimated)": "Réponse amont (billing-usage-gemini-estimated)",
|
||||
"Upstream Response (billing-usage-gemini)": "Réponse amont (billing-usage-gemini)",
|
||||
"Upstream Response (billing-usage-openai-estimated)": "Réponse amont (billing-usage-openai-estimated)",
|
||||
"Upstream Response (billing-usage-openai)": "Réponse amont (billing-usage-openai)",
|
||||
"upstream services integrated": "services en amont intégrés",
|
||||
"Upstream Updates": "Mises à jour en amont",
|
||||
"Upstream URL": "URL amont",
|
||||
@@ -4819,6 +4871,8 @@
|
||||
"Use authenticator code": "Utiliser le code de l'authentificateur",
|
||||
"Use backup code": "Utiliser un code de secours",
|
||||
"Use disk cache when request body exceeds this size": "Utiliser le cache disque quand le corps de requête dépasse cette taille",
|
||||
"Use exact client model names, separated by commas. Prefixes and wildcards are not supported.": "Utilisez les noms exacts des modèles client, séparés par des virgules. Les préfixes et jokers ne sont pas pris en charge.",
|
||||
"Use exact model names such as gpt-4o, or regex rules prefixed with re: such as re:^gemini-.": "Utilisez des noms de modèle exacts comme gpt-4o, ou des règles regex préfixées par re: comme re:^gemini-.",
|
||||
"Use external tools to extend capabilities": "Utiliser des outils externes pour étendre les capacités",
|
||||
"Use one available reset credit for this channel. The reset request is sent only after confirmation.": "Utilise un crédit de réinitialisation disponible pour ce canal. La demande n’est envoyée qu’après confirmation.",
|
||||
"Use one available reset credit to refresh the current Codex usage windows.": "Utilise un crédit de réinitialisation disponible pour actualiser les fenêtres d’utilisation Codex actuelles.",
|
||||
|
||||
Vendored
+54
@@ -207,6 +207,7 @@
|
||||
"Add rule group": "ルールグループを追加",
|
||||
"Add rules for a user group": "ユーザーグループにルールを追加",
|
||||
"Add selectable group": "選択可能なグループを追加",
|
||||
"Add split": "分岐を追加",
|
||||
"Add subscription": "サブスクリプションを追加",
|
||||
"Add tags...": "タグを追加...",
|
||||
"Add tier": "ティアを追加",
|
||||
@@ -617,6 +618,7 @@
|
||||
"Billing group = vip (the token has no group, so use the user group)": "課金グループ = vip(トークンにグループがないのでユーザーグループを使用)",
|
||||
"Billing History": "請求履歴",
|
||||
"Billing Mode": "課金モード",
|
||||
"Billing Path": "課金パス",
|
||||
"Billing Process": "課金プロセス",
|
||||
"Billing rule: each call is billed as the token group (falling back to the user group when the token has none). The base ratio always comes from that billing group, not from the user group. To give a user group a special price on another billing group, add an entry in the override matrix.": "課金ルール:各呼び出しはトークングループとして課金されます(トークンにグループがない場合はユーザーグループにフォールバック)。基本倍率は常にその課金グループから取得され、ユーザーグループの倍率は適用されません。特定のユーザーグループに別の課金グループでの特別価格を設定するには、上書きマトリクスにエントリを追加してください。",
|
||||
"Billing Source": "課金ソース",
|
||||
@@ -715,6 +717,7 @@
|
||||
"Caps the response length": "応答の長さを制限します",
|
||||
"Capture a reusable bundle of models, tags, or endpoints.": "モデル、タグ、またはエンドポイントの再利用可能なバンドルを保存。",
|
||||
"Card view": "カード表示",
|
||||
"Catch-all route must be last for the same incoming path": "同じ入力パスのキャッチオールルートは最後に配置してください",
|
||||
"Category": "カテゴリ",
|
||||
"Category Name": "分類名称",
|
||||
"Category name is required": "カテゴリ名は必須です",
|
||||
@@ -867,6 +870,9 @@
|
||||
"Click to view image": "クリックして画像を表示",
|
||||
"Client header value": "クライアントヘッダー値",
|
||||
"Client ID": "クライアントID",
|
||||
"Client model": "クライアント model",
|
||||
"Client model matching": "クライアント model のマッチング",
|
||||
"Client model matching help": "クライアント model マッチングのヘルプ",
|
||||
"Client Secret": "クライアントシークレット",
|
||||
"Close": "閉じる",
|
||||
"Close dialog": "ダイアログを閉じる",
|
||||
@@ -1446,6 +1452,7 @@
|
||||
"Drawing task records": "描画タスク記録",
|
||||
"Duplicate": "複製",
|
||||
"Duplicate group names: {{names}}": "重複するグループ名: {{names}}",
|
||||
"Duplicate model in route models": "ルートモデルに重複したモデルがあります",
|
||||
"Duplicate source model mappings are not allowed": "重複したソースモデルのマッピングは許可されていません",
|
||||
"Duplicate source model(s): {{models}}": "重複したソースモデル: {{models}}",
|
||||
"Duration": "所要時間",
|
||||
@@ -1460,6 +1467,7 @@
|
||||
"e.g. Basic Plan": "例:ベーシックプラン",
|
||||
"e.g. Clean tool parameters to avoid upstream validation errors": "例:ツールパラメータを整理して上流の検証エラーを回避",
|
||||
"e.g. example.com": "例: example.com",
|
||||
"e.g. gpt-4o, gemini-2.5-flash": "例: gpt-4o, gemini-2.5-flash",
|
||||
"e.g. llama3.1:8b": "例: llama3.1:8b",
|
||||
"e.g. My GitLab": "例: My GitLab",
|
||||
"e.g. my-gitlab": "例: my-gitlab",
|
||||
@@ -1717,6 +1725,7 @@
|
||||
"Everything configured for this group, in one place.": "このグループのすべての設定を一か所で確認できます。",
|
||||
"Exact": "完全一致",
|
||||
"Exact Match": "完全一致",
|
||||
"Exact match only and case-sensitive. Prefixes, regex, and * wildcards are not supported.": "完全一致のみで、大文字と小文字も区別します。プレフィックス、正規表現、* ワイルドカードは使えません。",
|
||||
"Example": "サンプル",
|
||||
"Example (all channels):": "例(全チャネル):",
|
||||
"Example (specific channels):": "例(特定チャネル):",
|
||||
@@ -1903,7 +1912,11 @@
|
||||
"Failed to update user": "ユーザーの更新に失敗しました",
|
||||
"Failure keywords": "失敗キーワード",
|
||||
"Fair": "公平",
|
||||
"Fallback": "フォールバック",
|
||||
"Fallback base URL": "フォールバック Base URL",
|
||||
"Fallback for remaining models": "残りのモデル用フォールバック",
|
||||
"Fallback must be last": "フォールバックは最後",
|
||||
"Fallback route": "フォールバックルート",
|
||||
"Fallback tier": "フォールバック段階",
|
||||
"FAQ": "FAQ",
|
||||
"FAQ added. Click \"Save Settings\" to apply.": "FAQ が追加されました。「設定を保存」をクリックして適用してください。",
|
||||
@@ -1941,6 +1954,7 @@
|
||||
"Fill Related Models": "関連モデルを入力",
|
||||
"Fill Template": "テンプレートを入力",
|
||||
"Fill Templates": "テンプレートを入力",
|
||||
"Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "クライアントリクエスト本文の完全な model 値を入力します。例: gpt-4o または gemini-2.5-flash。複数のモデルはカンマで区切ります。",
|
||||
"Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "OpenAI形式を利用するGemini/VertexチャネルにのみthoughtSignatureを付与します",
|
||||
"Filled {{count}} model(s)": "{{count}} 個のモデルを補完しました",
|
||||
"Filled {{count}} related model(s)": "{{count}} 個の関連モデルを補完しました",
|
||||
@@ -1982,6 +1996,7 @@
|
||||
"First token": "先頭トークン",
|
||||
"First/Last Frame to Video": "先頭/末尾フレームから動画",
|
||||
"Fix Abilities": "チャネル整合性を修復",
|
||||
"Fix order": "順序を修正",
|
||||
"Fixed abilities: {{success}} succeeded, {{fails}} failed": "チャネル整合性を修復しました:成功 {{success}} 件、失敗 {{fails}} 件",
|
||||
"Fixed price": "固定価格",
|
||||
"Fixed price (USD)": "固定価格 (USD)",
|
||||
@@ -2426,16 +2441,21 @@
|
||||
"Leave blank to keep the existing credential": "既存の認証情報を保持するには、空白のままにしてください",
|
||||
"Leave blank to keep the existing key": "空欄のままにすると既存のキーを保持します",
|
||||
"Leave blank unless rotating the secret": "シークレットをローテーションする場合を除き、空白のままにしてください",
|
||||
"Leave empty for fallback": "フォールバックは空のまま",
|
||||
"Leave empty for never expires": "期限切れなしにするには空のままにしてください",
|
||||
"Leave empty only for the final fallback split.": "空にできるのは最後のフォールバック分岐だけです。",
|
||||
"Leave empty to disable the agreement requirement. Supports Markdown, HTML, or a full URL to redirect users.": "利用規約の要件を無効にするには空のままにしてください。Markdown、HTML、またはユーザーをリダイレクトするための完全なURLをサポートします。",
|
||||
"Leave empty to disable the privacy policy requirement. Supports Markdown, HTML, or a full URL to redirect users.": "プライバシーポリシーの要件を無効にするには空のままにしてください。Markdown、HTML、またはユーザーをリダイレクトするための完全なURLをサポートします。",
|
||||
"Leave empty to disband the tag": "タグを解散するには空のままにしてください",
|
||||
"Leave empty to keep existing key": "空欄のままにすると既存のキーを保持します",
|
||||
"Leave empty to keep unchanged": "変更しない場合は空欄のまま",
|
||||
"Leave empty to match all models": "空欄の場合はすべてのモデルに一致",
|
||||
"Leave empty to use account email": "アカウントのメールアドレスを使用するには空のままにしてください",
|
||||
"Leave empty to use default": "デフォルトを使用する場合は空欄にしてください",
|
||||
"Leave empty to use system temp directory": "空欄でシステムの一時ディレクトリを使用",
|
||||
"Leave empty to use username": "ユーザー名を使用するには空のままにしてください",
|
||||
"Leave the final split empty as the fallback for models not matched above.": "上のルールに一致しないモデル用のフォールバックとして、最後の分岐を空にします。",
|
||||
"Leave this empty only for the final fallback split; it catches client models not matched above.": "空にできるのは最後のフォールバック分岐だけです。上の分岐で一致しないクライアントモデルを受けます。",
|
||||
"Left to Right": "左から右",
|
||||
"Legacy Format (JSON Object)": "旧形式(JSONオブジェクト)",
|
||||
"Legacy format must be a JSON object": "旧形式はJSONオブジェクトである必要があります",
|
||||
@@ -2482,6 +2502,9 @@
|
||||
"Loading...": "読み込み中...",
|
||||
"Local": "ローカル",
|
||||
"Local Billing": "ローカル課金",
|
||||
"Local Estimate (billing-usage-anthropic)": "ローカル推定 (billing-usage-anthropic)",
|
||||
"Local Estimate (billing-usage-gemini)": "ローカル推定 (billing-usage-gemini)",
|
||||
"Local Estimate (billing-usage-openai)": "ローカル推定 (billing-usage-openai)",
|
||||
"Local models": "ローカルモデル",
|
||||
"Locations": "場所",
|
||||
"Locked": "ロック済み",
|
||||
@@ -2548,7 +2571,9 @@
|
||||
"Match Value": "マッチ値",
|
||||
"Match Value (optional)": "マッチ値(任意)",
|
||||
"Matched": "一致",
|
||||
"Matched models": "一致モデル",
|
||||
"Matched Tier": "一致した階層",
|
||||
"Matches models not claimed by earlier splits.": "前の分岐で使われていないモデルに一致します。",
|
||||
"Matching Rules": "マッチングルール",
|
||||
"Max Disk Cache Size (MB)": "ディスクキャッシュ最大容量 (MB)",
|
||||
"Max Entries": "最大エントリ数",
|
||||
@@ -2662,12 +2687,15 @@
|
||||
"Model ratios reset successfully": "モデル比率が正常にリセットされました",
|
||||
"Model Regex": "モデル正規表現",
|
||||
"Model Regex (one per line)": "モデル正規表現(1行に1つ)",
|
||||
"Model regex cannot be empty": "モデル正規表現は空にできません",
|
||||
"Model scope": "モデル範囲",
|
||||
"Model selected": "選択済みモデル",
|
||||
"Model Square": "モデル広場",
|
||||
"Model Tags": "モデルタグ",
|
||||
"Model to use for testing": "テストに使用するモデル",
|
||||
"Model to use when testing channel connectivity": "チャネル接続性をテストする際に使用するモデル",
|
||||
"Model Version *": "モデルバージョン *",
|
||||
"Model-scoped only": "モデル指定のみ",
|
||||
"model(s) selected out of": "選択されたモデル",
|
||||
"model(s)? This action cannot be undone.": "モデルを削除しますか?この操作は元に戻せません。",
|
||||
"models": "モデル",
|
||||
@@ -2714,9 +2742,12 @@
|
||||
"Move": "移動",
|
||||
"Move a request header": "リクエストヘッダーを移動",
|
||||
"Move affiliate rewards to your main balance": "アフィリエイト報酬をメイン残高に移動する",
|
||||
"Move fallback to end": "フォールバックを最後へ",
|
||||
"Move Field": "フィールドを移動",
|
||||
"Move Header": "ヘッダーを移動",
|
||||
"Move Request Header": "リクエストヘッダーを移動",
|
||||
"Move route down": "ルートを下へ",
|
||||
"Move route up": "ルートを上へ",
|
||||
"Move source field to target field": "ソースフィールドをターゲットフィールドに移動",
|
||||
"ms": "ms",
|
||||
"Multi-key channel: Keys will be": "マルチキーチャネル: キーは",
|
||||
@@ -3050,6 +3081,7 @@
|
||||
"Only enabled parameters are sent with the request.": "有効なパラメータだけがリクエストに送信されます。",
|
||||
"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 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.": "成功したリクエストのみがこの制限にカウントされます。",
|
||||
@@ -3088,6 +3120,7 @@
|
||||
"OpenAI Rerank": "OpenAI 再ランク付け",
|
||||
"OpenAI Responses": "OpenAI レスポンス",
|
||||
"OpenAI Responses Compact": "OpenAI レスポンス圧縮",
|
||||
"OpenAI Responses to Gemini Generate Content": "OpenAI Responses から Gemini Generate Content",
|
||||
"OpenAI Responses to OpenAI Chat": "OpenAI Responses から OpenAI Chat",
|
||||
"OpenAI, Anthropic, etc.": "OpenAI、Anthropicなど",
|
||||
"OpenAI, Anthropic, Google, etc.": "OpenAI、Anthropic、Googleなど",
|
||||
@@ -3835,9 +3868,15 @@
|
||||
"Route": "ルート",
|
||||
"Route active": "ルート有効",
|
||||
"Route Description": "ルートの説明",
|
||||
"Route group": "ルートグループ",
|
||||
"Route is required": "ルートは必須です",
|
||||
"Route models must be unique for the same incoming path": "同じ入力パスではルートのモデルを一意にしてください",
|
||||
"Route, auth, and balance check in one place": "ルート、認証、残高確認を一か所に集約",
|
||||
"Routes": "ルート",
|
||||
"Routes with the same incoming path are matched by model. Leave the model scope empty only for the final fallback route.": "同じ入口パスのルートはモデルで照合されます。モデル範囲を空にできるのは最後のフォールバックルートだけです。",
|
||||
"Routes with the same incoming path are split by client model rules. Unmatched requests use the final fallback.": "同じ入口パスのルートはクライアント model ルールで分岐します。一致しないリクエストは最後のフォールバックを使います。",
|
||||
"Routes with the same incoming path are split by exact client model. Unmatched requests use the final fallback.": "同じ入口パスのルートは、クライアント model の完全一致で分岐します。一致しないリクエストは最後のフォールバックを使います。",
|
||||
"Routes with the same incoming path match exact client model names. Separate multiple models with commas, and leave only the final fallback empty.": "同じ入口パスのルートは、クライアントの正確なモデル名で一致します。複数のモデルはカンマで区切り、最後のフォールバックだけを空にします。",
|
||||
"Routing & Overrides": "ルーティングと上書き",
|
||||
"Routing Reliability": "ルーティング信頼性",
|
||||
"Routing Strategy": "ルーティング戦略",
|
||||
@@ -3862,6 +3901,7 @@
|
||||
"Rules": "ルール",
|
||||
"Rules JSON": "ルール JSON",
|
||||
"Rules JSON must be an array": "ルール JSON は配列である必要があります",
|
||||
"Rules match the original model value from the client request body.": "ルールはクライアントリクエスト本文の元の model 値に一致します。",
|
||||
"Run GC": "GC 実行",
|
||||
"Run tests for the selected models": "選択したモデルのテストを実行",
|
||||
"running": "実行中",
|
||||
@@ -4057,6 +4097,7 @@
|
||||
"Sensitive Words": "機密語",
|
||||
"Sent the API key to FluentRead.": "API キーを FluentRead に送信しました。",
|
||||
"Separate image/audio prices are enabled.": "画像/音声の個別料金が有効です。",
|
||||
"Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "複数のルールは半角カンマで区切ります。カンマが必要な正規表現は JSON Text に切り替えてください。",
|
||||
"Serve multiple users or teams with billing and quota control.": "課金とクォータ管理で複数のユーザーやチームにサービスを提供します。",
|
||||
"Server Address": "サーバーURL",
|
||||
"Server IP": "サーバー IP",
|
||||
@@ -4470,6 +4511,7 @@
|
||||
"This FAQ entry will be removed from the list.": "この FAQ 項目はリストから削除されます。",
|
||||
"This feature is experimental. Configuration format and behavior may change.": "この機能は実験的です。設定フォーマットや動作は変更される可能性があります。",
|
||||
"This feature requires server-side WeChat configuration": "この機能にはサーバー側のWeChat設定が必要です",
|
||||
"This field does not support wildcards or regular expressions.": "このフィールドではワイルドカードや正規表現は使えません。",
|
||||
"This historical record predates audit-info tracking and cannot be backfilled. The current instance already records server IP, callback IP, payment method, and system version for new top-ups going forward.": "このレコードは監査情報の記録に対応する前の履歴データのため、監査情報がありません。現在のバージョンではサーバーIP、コールバックIP、支払い方法、システムバージョンなどの監査情報を記録できますが、これらは今後新しく作成されるレコードにのみ適用され、過去のレコードを遡って補完することはできません。",
|
||||
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "注文作成時に、この識別子が決済バックエンドへ送信されます。Alipay は alipay、WeChat Pay は wxpay、Stripe は stripe を使ってください。カスタム値は決済サービス側で対応している必要があります。",
|
||||
"This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.": "このインスタンスは自動ホスト名を使用しています。マルチインスタンス管理のために、安定した一意の NODE_NAME を設定してください。",
|
||||
@@ -4536,9 +4578,13 @@
|
||||
"Timing": "所要時間",
|
||||
"Tip": "ヒント",
|
||||
"to access this resource.": "このリソースにアクセスするには。",
|
||||
"To Anthropic Messages": "Anthropic Messages へ",
|
||||
"to confirm": "確認する",
|
||||
"To Gemini Generate Content": "Gemini Generate Content へ",
|
||||
"To Lower": "小文字に変換",
|
||||
"To Lowercase": "小文字化",
|
||||
"To OpenAI Chat": "OpenAI Chat へ",
|
||||
"To OpenAI Responses": "OpenAI Responses へ",
|
||||
"to override billing when a user in one group uses a token of another group.": "あるグループのユーザーが別のグループのトークンを使用する場合に、請求を上書きするため。",
|
||||
"to the Models list so users can use them before the mapping sends traffic upstream.": "マッピングがトラフィックをアップストリームに送信する前にユーザーが使用できるように、モデルリストに追加します。",
|
||||
"To Upper": "大文字に変換",
|
||||
@@ -4786,6 +4832,12 @@
|
||||
"Upstream ratios fetched successfully": "アップストリーム比率が正常に取得されました",
|
||||
"Upstream Request ID": "上流リクエストID",
|
||||
"Upstream Response": "アップストリームレスポンス",
|
||||
"Upstream Response (billing-usage-anthropic-estimated)": "アップストリームレスポンス (billing-usage-anthropic-estimated)",
|
||||
"Upstream Response (billing-usage-anthropic)": "アップストリームレスポンス (billing-usage-anthropic)",
|
||||
"Upstream Response (billing-usage-gemini-estimated)": "アップストリームレスポンス (billing-usage-gemini-estimated)",
|
||||
"Upstream Response (billing-usage-gemini)": "アップストリームレスポンス (billing-usage-gemini)",
|
||||
"Upstream Response (billing-usage-openai-estimated)": "アップストリームレスポンス (billing-usage-openai-estimated)",
|
||||
"Upstream Response (billing-usage-openai)": "アップストリームレスポンス (billing-usage-openai)",
|
||||
"upstream services integrated": "アップストリームサービス連携",
|
||||
"Upstream Updates": "アップストリーム更新",
|
||||
"Upstream URL": "上流 URL",
|
||||
@@ -4819,6 +4871,8 @@
|
||||
"Use authenticator code": "認証コードを使用",
|
||||
"Use backup code": "バックアップコードを使用",
|
||||
"Use disk cache when request body exceeds this size": "リクエストボディがこのサイズを超えた場合にディスクキャッシュを使用",
|
||||
"Use exact client model names, separated by commas. Prefixes and wildcards are not supported.": "クライアントの正確なモデル名をカンマ区切りで入力します。プレフィックスやワイルドカードは使えません。",
|
||||
"Use exact model names such as gpt-4o, or regex rules prefixed with re: such as re:^gemini-.": "gpt-4o のような完全一致のモデル名、または re:^gemini- のように re: で始まる正規表現ルールを使えます。",
|
||||
"Use external tools to extend capabilities": "外部ツールを利用して機能を拡張",
|
||||
"Use one available reset credit for this channel. The reset request is sent only after confirmation.": "このチャンネルで利用可能なリセット回数を1回使用します。確認後にのみリセット要求を送信します。",
|
||||
"Use one available reset credit to refresh the current Codex usage windows.": "利用可能なリセット回数を1回使用して、現在の Codex 使用量ウィンドウを更新します。",
|
||||
|
||||
Vendored
+54
@@ -207,6 +207,7 @@
|
||||
"Add rule group": "Добавить группу правил",
|
||||
"Add rules for a user group": "Добавить правила для группы пользователей",
|
||||
"Add selectable group": "Добавить выбираемую группу",
|
||||
"Add split": "Добавить ветку",
|
||||
"Add subscription": "Добавить подписку",
|
||||
"Add tags...": "Добавить теги...",
|
||||
"Add tier": "Добавить уровень",
|
||||
@@ -617,6 +618,7 @@
|
||||
"Billing group = vip (the token has no group, so use the user group)": "Тарифная группа = vip (у токена нет группы, используем группу пользователя)",
|
||||
"Billing History": "История биллинга",
|
||||
"Billing Mode": "Режим биллинга",
|
||||
"Billing Path": "Путь тарификации",
|
||||
"Billing Process": "Процесс тарификации",
|
||||
"Billing rule: each call is billed as the token group (falling back to the user group when the token has none). The base ratio always comes from that billing group, not from the user group. To give a user group a special price on another billing group, add an entry in the override matrix.": "Правило тарификации: каждый вызов тарифицируется по группе токена (если у токена нет группы — по группе пользователя). Базовый коэффициент всегда берётся из этой тарифной группы, а не из группы пользователя. Чтобы задать группе пользователей особую цену для другой тарифной группы, добавьте запись в матрицу переопределений.",
|
||||
"Billing Source": "Источник биллинга",
|
||||
@@ -715,6 +717,7 @@
|
||||
"Caps the response length": "Ограничивает длину ответа",
|
||||
"Capture a reusable bundle of models, tags, or endpoints.": "Создайте повторно используемый набор моделей, тегов или конечных точек.",
|
||||
"Card view": "Карточки",
|
||||
"Catch-all route must be last for the same incoming path": "Резервный маршрут должен быть последним для этого входного пути",
|
||||
"Category": "Категория",
|
||||
"Category Name": "Название категории",
|
||||
"Category name is required": "Название категории обязательно",
|
||||
@@ -867,6 +870,9 @@
|
||||
"Click to view image": "Нажмите, чтобы просмотреть изображение",
|
||||
"Client header value": "Значение заголовка клиента",
|
||||
"Client ID": "ID клиента",
|
||||
"Client model": "Модель клиента",
|
||||
"Client model matching": "Сопоставление client model",
|
||||
"Client model matching help": "Справка по сопоставлению client model",
|
||||
"Client Secret": "Секрет клиента",
|
||||
"Close": "Закрыть",
|
||||
"Close dialog": "Закрыть диалог",
|
||||
@@ -1446,6 +1452,7 @@
|
||||
"Drawing task records": "Записи задач рисования",
|
||||
"Duplicate": "Дублировать",
|
||||
"Duplicate group names: {{names}}": "Повторяющиеся имена групп: {{names}}",
|
||||
"Duplicate model in route models": "В моделях маршрута есть дубликат модели",
|
||||
"Duplicate source model mappings are not allowed": "Повторяющиеся сопоставления исходных моделей не допускаются",
|
||||
"Duplicate source model(s): {{models}}": "Повторяющиеся исходные модели: {{models}}",
|
||||
"Duration": "Длительность",
|
||||
@@ -1460,6 +1467,7 @@
|
||||
"e.g. Basic Plan": "напр. Базовый план",
|
||||
"e.g. Clean tool parameters to avoid upstream validation errors": "напр. Очистить параметры инструментов во избежание ошибок валидации",
|
||||
"e.g. example.com": "напр. example.com",
|
||||
"e.g. gpt-4o, gemini-2.5-flash": "например, gpt-4o, gemini-2.5-flash",
|
||||
"e.g. llama3.1:8b": "например llama3.1:8b",
|
||||
"e.g. My GitLab": "например, My GitLab",
|
||||
"e.g. my-gitlab": "например, my-gitlab",
|
||||
@@ -1717,6 +1725,7 @@
|
||||
"Everything configured for this group, in one place.": "Все настройки этой группы в одном месте.",
|
||||
"Exact": "Точное",
|
||||
"Exact Match": "Точное совпадение",
|
||||
"Exact match only and case-sensitive. Prefixes, regex, and * wildcards are not supported.": "Только точное совпадение с учетом регистра. Префиксы, регулярные выражения и подстановки * не поддерживаются.",
|
||||
"Example": "Пример",
|
||||
"Example (all channels):": "Пример (все каналы):",
|
||||
"Example (specific channels):": "Пример (указанные каналы):",
|
||||
@@ -1903,7 +1912,11 @@
|
||||
"Failed to update user": "Не удалось обновить пользователя",
|
||||
"Failure keywords": "Ключевые слова сбоя",
|
||||
"Fair": "Удовлетворительно",
|
||||
"Fallback": "Резерв",
|
||||
"Fallback base URL": "Base URL fallback",
|
||||
"Fallback for remaining models": "Резерв для остальных моделей",
|
||||
"Fallback must be last": "Резервный маршрут последним",
|
||||
"Fallback route": "Резервный маршрут",
|
||||
"Fallback tier": "Резервный уровень",
|
||||
"FAQ": "Часто задаваемые вопросы",
|
||||
"FAQ added. Click \"Save Settings\" to apply.": "FAQ добавлен. Нажмите \"Сохранить настройки\" чтобы применить.",
|
||||
@@ -1941,6 +1954,7 @@
|
||||
"Fill Related Models": "Заполнить связанные модели",
|
||||
"Fill Template": "Заполнить шаблон",
|
||||
"Fill Templates": "Заполнить шаблоны",
|
||||
"Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "Укажите полное значение model из тела запроса клиента, например gpt-4o или gemini-2.5-flash. Несколько моделей разделяйте запятыми.",
|
||||
"Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "Заполнять thoughtSignature только для каналов Gemini/Vertex, использующих формат OpenAI",
|
||||
"Filled {{count}} model(s)": "Заполнено {{count}} моделей",
|
||||
"Filled {{count}} related model(s)": "Заполнено {{count}} связанных моделей",
|
||||
@@ -1982,6 +1996,7 @@
|
||||
"First token": "Первый токен",
|
||||
"First/Last Frame to Video": "Первый/последний кадр в видео",
|
||||
"Fix Abilities": "Восстановить согласованность каналов",
|
||||
"Fix order": "Исправить порядок",
|
||||
"Fixed abilities: {{success}} succeeded, {{fails}} failed": "Согласованность каналов восстановлена: успешно {{success}}, ошибок {{fails}}",
|
||||
"Fixed price": "Фиксированная цена",
|
||||
"Fixed price (USD)": "Фиксированная цена (USD)",
|
||||
@@ -2426,16 +2441,21 @@
|
||||
"Leave blank to keep the existing credential": "Оставьте пустым, чтобы сохранить существующие учетные данные",
|
||||
"Leave blank to keep the existing key": "Оставьте пустым, чтобы сохранить существующий ключ",
|
||||
"Leave blank unless rotating the secret": "Оставьте пустым, если не меняете секрет",
|
||||
"Leave empty for fallback": "Оставьте пустым для резерва",
|
||||
"Leave empty for never expires": "Оставьте пустым для бессрочного действия",
|
||||
"Leave empty only for the final fallback split.": "Оставляйте пустым только последнюю резервную ветку.",
|
||||
"Leave empty to disable the agreement requirement. Supports Markdown, HTML, or a full URL to redirect users.": "Оставьте пустым, чтобы отключить требование соглашения. Поддерживает Markdown, HTML или полный URL для перенаправления пользователей.",
|
||||
"Leave empty to disable the privacy policy requirement. Supports Markdown, HTML, or a full URL to redirect users.": "Оставьте пустым, чтобы отключить требование политики конфиденциальности. Поддерживает Markdown, HTML или полный URL для перенаправления пользователей.",
|
||||
"Leave empty to disband the tag": "Оставьте пустым, чтобы удалить тег",
|
||||
"Leave empty to keep existing key": "Оставьте пустым, чтобы сохранить существующий ключ",
|
||||
"Leave empty to keep unchanged": "Оставьте пустым, чтобы сохранить без изменений",
|
||||
"Leave empty to match all models": "Оставьте пустым, чтобы сопоставлять все модели",
|
||||
"Leave empty to use account email": "Оставьте пустым, чтобы использовать электронную почту учетной записи",
|
||||
"Leave empty to use default": "Оставьте пустым для использования по умолчанию",
|
||||
"Leave empty to use system temp directory": "Оставьте пустым для системного временного каталога",
|
||||
"Leave empty to use username": "Оставьте пустым, чтобы использовать имя пользователя",
|
||||
"Leave the final split empty as the fallback for models not matched above.": "Оставьте последний маршрут пустым как резервный для моделей, не совпавших выше.",
|
||||
"Leave this empty only for the final fallback split; it catches client models not matched above.": "Оставляйте пустым только последнюю резервную ветку; она принимает модели клиента, не совпавшие выше.",
|
||||
"Left to Right": "Слева направо",
|
||||
"Legacy Format (JSON Object)": "Старый формат (JSON-объект)",
|
||||
"Legacy format must be a JSON object": "Старый формат должен быть JSON-объектом",
|
||||
@@ -2482,6 +2502,9 @@
|
||||
"Loading...": "Загрузка...",
|
||||
"Local": "Локальный",
|
||||
"Local Billing": "Локальная тарификация",
|
||||
"Local Estimate (billing-usage-anthropic)": "Локальная оценка (billing-usage-anthropic)",
|
||||
"Local Estimate (billing-usage-gemini)": "Локальная оценка (billing-usage-gemini)",
|
||||
"Local Estimate (billing-usage-openai)": "Локальная оценка (billing-usage-openai)",
|
||||
"Local models": "Локальные модели",
|
||||
"Locations": "Местоположения",
|
||||
"Locked": "Заблокировано",
|
||||
@@ -2548,7 +2571,9 @@
|
||||
"Match Value": "Значение сопоставления",
|
||||
"Match Value (optional)": "Значение сопоставления (необязательно)",
|
||||
"Matched": "Совпадение",
|
||||
"Matched models": "Модели для сопоставления",
|
||||
"Matched Tier": "Подходящий уровень",
|
||||
"Matches models not claimed by earlier splits.": "Совпадает с моделями, не занятыми предыдущими ветками.",
|
||||
"Matching Rules": "Правила сопоставления",
|
||||
"Max Disk Cache Size (MB)": "Макс. размер дискового кэша (МБ)",
|
||||
"Max Entries": "Макс. записей",
|
||||
@@ -2662,12 +2687,15 @@
|
||||
"Model ratios reset successfully": "Соотношения моделей успешно сброшены",
|
||||
"Model Regex": "Регулярное выражение модели",
|
||||
"Model Regex (one per line)": "Регулярное выражение модели (по одному на строку)",
|
||||
"Model regex cannot be empty": "Регулярное выражение модели не может быть пустым",
|
||||
"Model scope": "Область моделей",
|
||||
"Model selected": "Модель выбрана",
|
||||
"Model Square": "Витрина моделей",
|
||||
"Model Tags": "Теги моделей",
|
||||
"Model to use for testing": "Модель для использования при тестировании",
|
||||
"Model to use when testing channel connectivity": "Модель для использования при тестировании подключения канала",
|
||||
"Model Version *": "Версия модели *",
|
||||
"Model-scoped only": "Только по моделям",
|
||||
"model(s) selected out of": "модель(и) выбрано из",
|
||||
"model(s)? This action cannot be undone.": "модель(и)? Это действие нельзя отменить.",
|
||||
"models": "моделей",
|
||||
@@ -2714,9 +2742,12 @@
|
||||
"Move": "Переместить",
|
||||
"Move a request header": "Переместить заголовок запроса",
|
||||
"Move affiliate rewards to your main balance": "Перевести партнерские вознаграждения на основной баланс",
|
||||
"Move fallback to end": "Переместить резерв в конец",
|
||||
"Move Field": "Переместить поле",
|
||||
"Move Header": "Переместить заголовок",
|
||||
"Move Request Header": "Переместить заголовок запроса",
|
||||
"Move route down": "Переместить маршрут вниз",
|
||||
"Move route up": "Переместить маршрут вверх",
|
||||
"Move source field to target field": "Переместить исходное поле в целевое",
|
||||
"ms": "мс",
|
||||
"Multi-key channel: Keys will be": "Многоключевой канал: Ключи будут",
|
||||
@@ -3050,6 +3081,7 @@
|
||||
"Only enabled parameters are sent with the request.": "С запросом отправляются только включенные параметры.",
|
||||
"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 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.": "Только успешные запросы учитываются в этом лимите.",
|
||||
@@ -3088,6 +3120,7 @@
|
||||
"OpenAI Rerank": "Реранжирование OpenAI",
|
||||
"OpenAI Responses": "Ответы OpenAI",
|
||||
"OpenAI Responses Compact": "Компактные ответы OpenAI",
|
||||
"OpenAI Responses to Gemini Generate Content": "OpenAI Responses в Gemini Generate Content",
|
||||
"OpenAI Responses to OpenAI Chat": "OpenAI Responses в OpenAI Chat",
|
||||
"OpenAI, Anthropic, etc.": "OpenAI, Anthropic и т.д.",
|
||||
"OpenAI, Anthropic, Google, etc.": "OpenAI, Anthropic, Google и т.д.",
|
||||
@@ -3835,9 +3868,15 @@
|
||||
"Route": "Маршрут",
|
||||
"Route active": "Маршрут активен",
|
||||
"Route Description": "Описание маршрута",
|
||||
"Route group": "Группа маршрутов",
|
||||
"Route is required": "Маршрут обязателен",
|
||||
"Route models must be unique for the same incoming path": "Модели маршрутов для одного входного пути должны быть уникальными",
|
||||
"Route, auth, and balance check in one place": "Маршрут, аутентификация и баланс в одном месте",
|
||||
"Routes": "Маршруты",
|
||||
"Routes with the same incoming path are matched by model. Leave the model scope empty only for the final fallback route.": "Маршруты с одним входным путем сопоставляются по модели. Оставляйте область моделей пустой только для последнего резервного маршрута.",
|
||||
"Routes with the same incoming path are split by client model rules. Unmatched requests use the final fallback.": "Маршруты с одним входным путем разделяются правилами client model. Неподходящие запросы используют последний резервный маршрут.",
|
||||
"Routes with the same incoming path are split by exact client model. Unmatched requests use the final fallback.": "Маршруты с одним входным путем разделяются по точной модели клиента. Несовпавшие запросы идут в последний резерв.",
|
||||
"Routes with the same incoming path match exact client model names. Separate multiple models with commas, and leave only the final fallback empty.": "Маршруты с одним входным путем сопоставляются с точными именами моделей клиента. Несколько моделей разделяйте запятыми, пустым оставляйте только последний резерв.",
|
||||
"Routing & Overrides": "Маршрутизация и переопределения",
|
||||
"Routing Reliability": "Надежность маршрутизации",
|
||||
"Routing Strategy": "Стратегия маршрутизации",
|
||||
@@ -3862,6 +3901,7 @@
|
||||
"Rules": "Правила",
|
||||
"Rules JSON": "Правила JSON",
|
||||
"Rules JSON must be an array": "JSON правил должен быть массивом",
|
||||
"Rules match the original model value from the client request body.": "Правила сопоставляются с исходным значением model из тела клиентского запроса.",
|
||||
"Run GC": "Запустить GC",
|
||||
"Run tests for the selected models": "Запустить тесты для выбранных моделей",
|
||||
"running": "выполняется",
|
||||
@@ -4057,6 +4097,7 @@
|
||||
"Sensitive Words": "Чувствительные слова",
|
||||
"Sent the API key to FluentRead.": "API-ключ отправлен в FluentRead.",
|
||||
"Separate image/audio prices are enabled.": "Отдельные цены для изображений и аудио включены.",
|
||||
"Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "Разделяйте несколько правил английскими запятыми. Если regex нужны запятые, переключитесь на JSON Text.",
|
||||
"Serve multiple users or teams with billing and quota control.": "Обслуживание нескольких пользователей или команд с управлением биллингом и квотами.",
|
||||
"Server Address": "Адрес сервера",
|
||||
"Server IP": "IP сервера",
|
||||
@@ -4470,6 +4511,7 @@
|
||||
"This FAQ entry will be removed from the list.": "Эта запись FAQ будет удалена из списка.",
|
||||
"This feature is experimental. Configuration format and behavior may change.": "Эта функция является экспериментальной. Формат конфигурации и поведение могут измениться.",
|
||||
"This feature requires server-side WeChat configuration": "Эта функция требует серверной конфигурации WeChat",
|
||||
"This field does not support wildcards or regular expressions.": "Это поле не поддерживает подстановочные знаки или регулярные выражения.",
|
||||
"This historical record predates audit-info tracking and cannot be backfilled. The current instance already records server IP, callback IP, payment method, and system version for new top-ups going forward.": "Эта историческая запись была создана до появления функции аудита и не содержит данных аудита. Текущая версия уже поддерживает запись IP-адреса сервера, IP обратного вызова, способа оплаты и версии системы, но эти поля будут заполняться только в новых записях — восполнить их в старых записях задним числом невозможно.",
|
||||
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "Этот идентификатор отправляется в платежный backend при создании заказа. Для Alipay используйте alipay, для WeChat Pay — wxpay, для Stripe — stripe. Пользовательские значения должны поддерживаться вашим платежным провайдером.",
|
||||
"This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.": "Этот экземпляр использует автоматическое имя хоста. Задайте стабильное уникальное значение NODE_NAME для управления несколькими экземплярами.",
|
||||
@@ -4536,9 +4578,13 @@
|
||||
"Timing": "Время",
|
||||
"Tip": "Совет",
|
||||
"to access this resource.": "для доступа к этому ресурсу.",
|
||||
"To Anthropic Messages": "В Anthropic Messages",
|
||||
"to confirm": "для подтверждения",
|
||||
"To Gemini Generate Content": "В Gemini Generate Content",
|
||||
"To Lower": "В нижний регистр",
|
||||
"To Lowercase": "В нижний регистр",
|
||||
"To OpenAI Chat": "В OpenAI Chat",
|
||||
"To OpenAI Responses": "В OpenAI Responses",
|
||||
"to override billing when a user in one group uses a token of another group.": "для переопределения выставления счетов, когда пользователь одной группы использует токен другой группы.",
|
||||
"to the Models list so users can use them before the mapping sends traffic upstream.": "в список моделей, чтобы пользователи могли использовать их до того, как сопоставление отправит трафик выше по течению.",
|
||||
"To Upper": "В верхний регистр",
|
||||
@@ -4786,6 +4832,12 @@
|
||||
"Upstream ratios fetched successfully": "Коэффициенты upstream успешно получены",
|
||||
"Upstream Request ID": "ID вышестоящего запроса",
|
||||
"Upstream Response": "Ответ Upstream",
|
||||
"Upstream Response (billing-usage-anthropic-estimated)": "Ответ upstream (billing-usage-anthropic-estimated)",
|
||||
"Upstream Response (billing-usage-anthropic)": "Ответ upstream (billing-usage-anthropic)",
|
||||
"Upstream Response (billing-usage-gemini-estimated)": "Ответ upstream (billing-usage-gemini-estimated)",
|
||||
"Upstream Response (billing-usage-gemini)": "Ответ upstream (billing-usage-gemini)",
|
||||
"Upstream Response (billing-usage-openai-estimated)": "Ответ upstream (billing-usage-openai-estimated)",
|
||||
"Upstream Response (billing-usage-openai)": "Ответ upstream (billing-usage-openai)",
|
||||
"upstream services integrated": "интеграций с вышестоящими сервисами",
|
||||
"Upstream Updates": "Обновления вышестоящих моделей",
|
||||
"Upstream URL": "URL вышестоящего сервиса",
|
||||
@@ -4819,6 +4871,8 @@
|
||||
"Use authenticator code": "Использовать код аутентификатора",
|
||||
"Use backup code": "Использовать резервный код",
|
||||
"Use disk cache when request body exceeds this size": "Использовать дисковый кэш, когда тело запроса превышает этот размер",
|
||||
"Use exact client model names, separated by commas. Prefixes and wildcards are not supported.": "Укажите точные имена моделей клиента через запятую. Префиксы и подстановочные знаки не поддерживаются.",
|
||||
"Use exact model names such as gpt-4o, or regex rules prefixed with re: such as re:^gemini-.": "Используйте точные имена моделей, например gpt-4o, или regex-правила с префиксом re:, например re:^gemini-.",
|
||||
"Use external tools to extend capabilities": "Использовать внешние инструменты для расширения возможностей",
|
||||
"Use one available reset credit for this channel. The reset request is sent only after confirmation.": "Для этого канала будет использован один доступный сброс. Запрос отправляется только после подтверждения.",
|
||||
"Use one available reset credit to refresh the current Codex usage windows.": "Использует один доступный сброс, чтобы обновить текущие окна использования Codex.",
|
||||
|
||||
Vendored
+54
@@ -207,6 +207,7 @@
|
||||
"Add rule group": "Thêm nhóm quy tắc",
|
||||
"Add rules for a user group": "Thêm quy tắc cho nhóm người dùng",
|
||||
"Add selectable group": "Thêm nhóm có thể chọn",
|
||||
"Add split": "Thêm nhánh",
|
||||
"Add subscription": "Thêm đăng ký",
|
||||
"Add tags...": "Thêm thẻ...",
|
||||
"Add tier": "Thêm bậc",
|
||||
@@ -617,6 +618,7 @@
|
||||
"Billing group = vip (the token has no group, so use the user group)": "Nhóm tính phí = vip (token không có nhóm nên dùng nhóm người dùng)",
|
||||
"Billing History": "Lịch sử thanh toán",
|
||||
"Billing Mode": "Chế độ thanh toán",
|
||||
"Billing Path": "Đường dẫn tính phí",
|
||||
"Billing Process": "Quá trình tính phí",
|
||||
"Billing rule: each call is billed as the token group (falling back to the user group when the token has none). The base ratio always comes from that billing group, not from the user group. To give a user group a special price on another billing group, add an entry in the override matrix.": "Quy tắc tính phí: mỗi cuộc gọi được tính phí theo nhóm token (nếu token không có nhóm thì dùng nhóm người dùng). Hệ số cơ bản luôn lấy từ nhóm tính phí đó, không phải từ nhóm người dùng. Để cho một nhóm người dùng giá đặc biệt trên nhóm tính phí khác, hãy thêm mục vào ma trận ghi đè.",
|
||||
"Billing Source": "Nguồn thanh toán",
|
||||
@@ -715,6 +717,7 @@
|
||||
"Caps the response length": "Giới hạn độ dài phản hồi",
|
||||
"Capture a reusable bundle of models, tags, or endpoints.": "Đóng gói một bộ có thể tái sử dụng gồm các mô hình, thẻ hoặc điểm cuối.",
|
||||
"Card view": "Dạng thẻ",
|
||||
"Catch-all route must be last for the same incoming path": "Tuyến dự phòng phải đứng cuối cho cùng đường dẫn đầu vào",
|
||||
"Category": "Danh mục",
|
||||
"Category Name": "Tên danh mục",
|
||||
"Category name is required": "Tên danh mục là bắt buộc",
|
||||
@@ -867,6 +870,9 @@
|
||||
"Click to view image": "Nhấp để xem hình ảnh",
|
||||
"Client header value": "Giá trị header client",
|
||||
"Client ID": "Mã khách hàng",
|
||||
"Client model": "Model phía client",
|
||||
"Client model matching": "Khớp client model",
|
||||
"Client model matching help": "Trợ giúp khớp client model",
|
||||
"Client Secret": "Bí mật máy khách",
|
||||
"Close": "Đóng",
|
||||
"Close dialog": "Đóng hộp thoại",
|
||||
@@ -1446,6 +1452,7 @@
|
||||
"Drawing task records": "Lịch sử tác vụ vẽ",
|
||||
"Duplicate": "Nhân bản",
|
||||
"Duplicate group names: {{names}}": "Tên nhóm bị trùng: {{names}}",
|
||||
"Duplicate model in route models": "Mô hình bị lặp trong danh sách mô hình tuyến",
|
||||
"Duplicate source model mappings are not allowed": "Không cho phép ánh xạ mô hình nguồn trùng lặp",
|
||||
"Duplicate source model(s): {{models}}": "Mô hình nguồn trùng lặp: {{models}}",
|
||||
"Duration": "Thời lượng",
|
||||
@@ -1460,6 +1467,7 @@
|
||||
"e.g. Basic Plan": "ví dụ: Gói cơ bản",
|
||||
"e.g. Clean tool parameters to avoid upstream validation errors": "ví dụ: Dọn dẹp tham số công cụ để tránh lỗi xác thực upstream",
|
||||
"e.g. example.com": "ví dụ example.com",
|
||||
"e.g. gpt-4o, gemini-2.5-flash": "ví dụ gpt-4o, gemini-2.5-flash",
|
||||
"e.g. llama3.1:8b": "ví dụ: llama3.1:8b",
|
||||
"e.g. My GitLab": "ví dụ: GitLab của tôi",
|
||||
"e.g. my-gitlab": "ví dụ: my-gitlab",
|
||||
@@ -1717,6 +1725,7 @@
|
||||
"Everything configured for this group, in one place.": "Toàn bộ cấu hình của nhóm này, tại một nơi.",
|
||||
"Exact": "Chính xác",
|
||||
"Exact Match": "Khớp chính xác",
|
||||
"Exact match only and case-sensitive. Prefixes, regex, and * wildcards are not supported.": "Chỉ khớp chính xác và có phân biệt hoa thường. Không hỗ trợ tiền tố, regex hoặc ký tự đại diện *.",
|
||||
"Example": "Ví dụ",
|
||||
"Example (all channels):": "Ví dụ (tất cả kênh):",
|
||||
"Example (specific channels):": "Ví dụ (kênh cụ thể):",
|
||||
@@ -1903,7 +1912,11 @@
|
||||
"Failed to update user": "Không thể cập nhật người dùng",
|
||||
"Failure keywords": "Từ khóa thất bại",
|
||||
"Fair": "Công bằng",
|
||||
"Fallback": "Dự phòng",
|
||||
"Fallback base URL": "Base URL fallback",
|
||||
"Fallback for remaining models": "Dự phòng cho mô hình còn lại",
|
||||
"Fallback must be last": "Dự phòng phải cuối",
|
||||
"Fallback route": "Tuyến dự phòng",
|
||||
"Fallback tier": "Tầng dự phòng",
|
||||
"FAQ": "FAQ",
|
||||
"FAQ added. Click \"Save Settings\" to apply.": "Đã thêm FAQ. Nhấp \"Lưu cài đặt\" để áp dụng.",
|
||||
@@ -1941,6 +1954,7 @@
|
||||
"Fill Related Models": "Điền Mô hình Liên quan",
|
||||
"Fill Template": "Điền Mẫu",
|
||||
"Fill Templates": "Điền mẫu",
|
||||
"Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "Nhập đầy đủ giá trị model trong body yêu cầu của client, ví dụ gpt-4o hoặc gemini-2.5-flash. Ngăn cách nhiều model bằng dấu phẩy.",
|
||||
"Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "Điền thoughtSignature chỉ dành cho các kênh Gemini/Vertex sử dụng định dạng OpenAI",
|
||||
"Filled {{count}} model(s)": "Đã điền {{count}} mô hình",
|
||||
"Filled {{count}} related model(s)": "Đã điền {{count}} mô hình liên quan",
|
||||
@@ -1982,6 +1996,7 @@
|
||||
"First token": "Token đầu",
|
||||
"First/Last Frame to Video": "Khung đầu/cuối sang video",
|
||||
"Fix Abilities": "Sửa tính nhất quán kênh",
|
||||
"Fix order": "Sửa thứ tự",
|
||||
"Fixed abilities: {{success}} succeeded, {{fails}} failed": "Đã sửa tính nhất quán kênh: {{success}} thành công, {{fails}} thất bại",
|
||||
"Fixed price": "Giá cố định",
|
||||
"Fixed price (USD)": "Giá cố định (USD)",
|
||||
@@ -2426,16 +2441,21 @@
|
||||
"Leave blank to keep the existing credential": "Để trống để giữ thông tin xác thực hiện có",
|
||||
"Leave blank to keep the existing key": "Để trống để giữ khóa hiện có",
|
||||
"Leave blank unless rotating the secret": "Để trống trừ khi xoay vòng bí mật",
|
||||
"Leave empty for fallback": "Để trống cho dự phòng",
|
||||
"Leave empty for never expires": "Để trống để không bao giờ hết hạn",
|
||||
"Leave empty only for the final fallback split.": "Chỉ để trống cho nhánh dự phòng cuối cùng.",
|
||||
"Leave empty to disable the agreement requirement. Supports Markdown, HTML, or a full URL to redirect users.": "Để trống để tắt yêu cầu đồng ý. Hỗ trợ Markdown, HTML hoặc một URL đầy đủ để chuyển hướng người dùng.",
|
||||
"Leave empty to disable the privacy policy requirement. Supports Markdown, HTML, or a full URL to redirect users.": "Để trống để vô hiệu hóa yêu cầu chính sách bảo mật. Hỗ trợ Markdown, HTML hoặc một URL đầy đủ để chuyển hướng người dùng.",
|
||||
"Leave empty to disband the tag": "Để trống để giải tán thẻ",
|
||||
"Leave empty to keep existing key": "Để trống để giữ khóa hiện có",
|
||||
"Leave empty to keep unchanged": "Để trống để giữ nguyên",
|
||||
"Leave empty to match all models": "Để trống để khớp tất cả mô hình",
|
||||
"Leave empty to use account email": "Để trống để sử dụng email tài khoản",
|
||||
"Leave empty to use default": "Để trống để sử dụng mặc định",
|
||||
"Leave empty to use system temp directory": "Để trống để sử dụng thư mục tạm của hệ thống",
|
||||
"Leave empty to use username": "Để trống để sử dụng tên người dùng",
|
||||
"Leave the final split empty as the fallback for models not matched above.": "Để nhánh cuối trống làm dự phòng cho các model chưa khớp ở trên.",
|
||||
"Leave this empty only for the final fallback split; it catches client models not matched above.": "Chỉ để trống cho nhánh dự phòng cuối cùng; nhánh này nhận các model client chưa khớp ở trên.",
|
||||
"Left to Right": "Trái sang phải",
|
||||
"Legacy Format (JSON Object)": "Định dạng cũ (đối tượng JSON)",
|
||||
"Legacy format must be a JSON object": "Định dạng cũ phải là đối tượng JSON",
|
||||
@@ -2482,6 +2502,9 @@
|
||||
"Loading...": "Đang tải...",
|
||||
"Local": "Địa phương",
|
||||
"Local Billing": "Thanh toán nội địa",
|
||||
"Local Estimate (billing-usage-anthropic)": "Ước tính cục bộ (billing-usage-anthropic)",
|
||||
"Local Estimate (billing-usage-gemini)": "Ước tính cục bộ (billing-usage-gemini)",
|
||||
"Local Estimate (billing-usage-openai)": "Ước tính cục bộ (billing-usage-openai)",
|
||||
"Local models": "Mô hình cục bộ",
|
||||
"Locations": "Vị trí",
|
||||
"Locked": "Đã khóa",
|
||||
@@ -2548,7 +2571,9 @@
|
||||
"Match Value": "Giá trị khớp",
|
||||
"Match Value (optional)": "Giá trị khớp (tùy chọn)",
|
||||
"Matched": "Đã khớp",
|
||||
"Matched models": "Mô hình khớp",
|
||||
"Matched Tier": "Bậc khớp",
|
||||
"Matches models not claimed by earlier splits.": "Khớp các mô hình chưa được nhánh trước nhận.",
|
||||
"Matching Rules": "Quy tắc khớp",
|
||||
"Max Disk Cache Size (MB)": "Dung lượng tối đa bộ nhớ đệm đĩa (MB)",
|
||||
"Max Entries": "Số mục tối đa",
|
||||
@@ -2662,12 +2687,15 @@
|
||||
"Model ratios reset successfully": "Tỷ lệ mô hình đã được đặt lại thành công",
|
||||
"Model Regex": "Regex mô hình",
|
||||
"Model Regex (one per line)": "Regex mô hình (mỗi dòng một mục)",
|
||||
"Model regex cannot be empty": "Regex model không được để trống",
|
||||
"Model scope": "Phạm vi mô hình",
|
||||
"Model selected": "Đã chọn mô hình",
|
||||
"Model Square": "Quảng trường mô hình",
|
||||
"Model Tags": "Thẻ mô hình",
|
||||
"Model to use for testing": "Mô hình dùng để kiểm thử",
|
||||
"Model to use when testing channel connectivity": "Mô hình để sử dụng khi kiểm tra kết nối kênh",
|
||||
"Model Version *": "Phiên bản mô hình *",
|
||||
"Model-scoped only": "Chỉ theo mô hình",
|
||||
"model(s) selected out of": "mô hình(s) được chọn trong số",
|
||||
"model(s)? This action cannot be undone.": "mô hình(s)? Hành động này không thể hoàn tác.",
|
||||
"models": "mô hình",
|
||||
@@ -2714,9 +2742,12 @@
|
||||
"Move": "Di chuyển",
|
||||
"Move a request header": "Di chuyển header yêu cầu",
|
||||
"Move affiliate rewards to your main balance": "Chuyển phần thưởng liên kết vào số dư chính của bạn",
|
||||
"Move fallback to end": "Đưa dự phòng xuống cuối",
|
||||
"Move Field": "Di chuyển trường",
|
||||
"Move Header": "Di chuyển tiêu đề",
|
||||
"Move Request Header": "Di chuyển header yêu cầu",
|
||||
"Move route down": "Di chuyển tuyến xuống",
|
||||
"Move route up": "Di chuyển tuyến lên",
|
||||
"Move source field to target field": "Di chuyển trường nguồn sang trường đích",
|
||||
"ms": "ms",
|
||||
"Multi-key channel: Keys will be": "Kênh đa khóa: Các khóa sẽ là",
|
||||
@@ -3050,6 +3081,7 @@
|
||||
"Only enabled parameters are sent with the request.": "Chỉ các tham số đã bật mới được gửi trong yêu cầu.",
|
||||
"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 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.",
|
||||
@@ -3088,6 +3120,7 @@
|
||||
"OpenAI Rerank": "OpenAI Rerank",
|
||||
"OpenAI Responses": "OpenAI Responses",
|
||||
"OpenAI Responses Compact": "OpenAI Responses Compact",
|
||||
"OpenAI Responses to Gemini Generate Content": "OpenAI Responses sang Gemini Generate Content",
|
||||
"OpenAI Responses to OpenAI Chat": "OpenAI Responses sang OpenAI Chat",
|
||||
"OpenAI, Anthropic, etc.": "OpenAI, Anthropic, v.v.",
|
||||
"OpenAI, Anthropic, Google, etc.": "OpenAI, Anthropic, Google, v.v.",
|
||||
@@ -3835,9 +3868,15 @@
|
||||
"Route": "Tuyến đường",
|
||||
"Route active": "Tuyến đang hoạt động",
|
||||
"Route Description": "Mô tả lộ trình",
|
||||
"Route group": "Nhóm tuyến",
|
||||
"Route is required": "Đường dẫn là bắt buộc",
|
||||
"Route models must be unique for the same incoming path": "Các mô hình tuyến phải là duy nhất cho cùng đường dẫn đầu vào",
|
||||
"Route, auth, and balance check in one place": "Kiểm tra tuyến, xác thực và số dư ở cùng một nơi",
|
||||
"Routes": "Route",
|
||||
"Routes with the same incoming path are matched by model. Leave the model scope empty only for the final fallback route.": "Các tuyến có cùng đường dẫn vào được khớp theo mô hình. Chỉ để trống phạm vi mô hình cho tuyến dự phòng cuối cùng.",
|
||||
"Routes with the same incoming path are split by client model rules. Unmatched requests use the final fallback.": "Các route có cùng đường vào được phân nhánh theo quy tắc client model. Yêu cầu không khớp dùng nhánh dự phòng cuối.",
|
||||
"Routes with the same incoming path are split by exact client model. Unmatched requests use the final fallback.": "Các tuyến có cùng đường dẫn vào được tách theo model client chính xác. Yêu cầu chưa khớp sẽ dùng nhánh dự phòng cuối cùng.",
|
||||
"Routes with the same incoming path match exact client model names. Separate multiple models with commas, and leave only the final fallback empty.": "Các tuyến có cùng đường dẫn vào khớp theo tên model chính xác từ yêu cầu client. Ngăn cách nhiều model bằng dấu phẩy và chỉ để trống nhánh dự phòng cuối cùng.",
|
||||
"Routing & Overrides": "Định tuyến & ghi đè",
|
||||
"Routing Reliability": "Độ tin cậy định tuyến",
|
||||
"Routing Strategy": "Chiến lược định tuyến",
|
||||
@@ -3862,6 +3901,7 @@
|
||||
"Rules": "Quy tắc",
|
||||
"Rules JSON": "JSON quy tắc",
|
||||
"Rules JSON must be an array": "JSON quy tắc phải là một mảng",
|
||||
"Rules match the original model value from the client request body.": "Quy tắc khớp với giá trị model gốc trong thân yêu cầu của client.",
|
||||
"Run GC": "Chạy GC",
|
||||
"Run tests for the selected models": "Chạy kiểm thử cho các mô hình đã chọn",
|
||||
"running": "đang chạy",
|
||||
@@ -4057,6 +4097,7 @@
|
||||
"Sensitive Words": "Từ ngữ nhạy cảm",
|
||||
"Sent the API key to FluentRead.": "Đã gửi khóa API đến FluentRead.",
|
||||
"Separate image/audio prices are enabled.": "Giá riêng cho hình ảnh/âm thanh đã được bật.",
|
||||
"Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "Tách nhiều quy tắc bằng dấu phẩy tiếng Anh. Với regex cần dấu phẩy, hãy chuyển sang JSON Text.",
|
||||
"Serve multiple users or teams with billing and quota control.": "Phục vụ nhiều người dùng hoặc nhóm với quản lý thanh toán và hạn mức.",
|
||||
"Server Address": "Địa chỉ máy chủ",
|
||||
"Server IP": "IP máy chủ",
|
||||
@@ -4470,6 +4511,7 @@
|
||||
"This FAQ entry will be removed from the list.": "Mục FAQ này sẽ bị xóa khỏi danh sách.",
|
||||
"This feature is experimental. Configuration format and behavior may change.": "Tính năng này đang ở giai đoạn thử nghiệm. Định dạng cấu hình và hành vi có thể thay đổi.",
|
||||
"This feature requires server-side WeChat configuration": "Tính năng này yêu cầu cấu hình WeChat phía máy chủ",
|
||||
"This field does not support wildcards or regular expressions.": "Trường này không hỗ trợ ký tự đại diện hoặc biểu thức chính quy.",
|
||||
"This historical record predates audit-info tracking and cannot be backfilled. The current instance already records server IP, callback IP, payment method, and system version for new top-ups going forward.": "Bản ghi lịch sử này được tạo trước khi tính năng thông tin kiểm toán ra đời nên thiếu dữ liệu kiểm toán. Phiên bản hiện tại đã hỗ trợ ghi lại IP máy chủ, IP gọi lại, phương thức thanh toán và phiên bản hệ thống, nhưng các trường này chỉ được ghi cho các bản ghi mới về sau — không thể bổ sung hồi tố cho bản ghi cũ.",
|
||||
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "Mã định danh này được gửi tới backend thanh toán khi tạo đơn hàng. Dùng alipay cho Alipay, wxpay cho WeChat Pay, stripe cho Stripe. Giá trị tùy chỉnh phải được nhà cung cấp thanh toán hỗ trợ.",
|
||||
"This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.": "Phiên bản này đang dùng hostname tự động. Hãy đặt NODE_NAME thành một giá trị ổn định và duy nhất để quản lý nhiều phiên bản.",
|
||||
@@ -4536,9 +4578,13 @@
|
||||
"Timing": "Thời gian",
|
||||
"Tip": "Mẹo",
|
||||
"to access this resource.": "để truy cập tài nguyên này.",
|
||||
"To Anthropic Messages": "Sang Anthropic Messages",
|
||||
"to confirm": "Chờ xác nhận",
|
||||
"To Gemini Generate Content": "Sang Gemini Generate Content",
|
||||
"To Lower": "Chữ thường",
|
||||
"To Lowercase": "Chuyển chữ thường",
|
||||
"To OpenAI Chat": "Sang OpenAI Chat",
|
||||
"To OpenAI Responses": "Sang OpenAI Responses",
|
||||
"to override billing when a user in one group uses a token of another group.": "để ghi đè việc thanh toán khi một người dùng trong một nhóm sử dụng token của một nhóm khác.",
|
||||
"to the Models list so users can use them before the mapping sends traffic upstream.": "vào danh sách Mô hình để người dùng có thể sử dụng chúng trước khi ánh xạ gửi lưu lượng truy cập lên phía trên.",
|
||||
"To Upper": "Chữ hoa",
|
||||
@@ -4786,6 +4832,12 @@
|
||||
"Upstream ratios fetched successfully": "Đã lấy tỷ lệ upstream thành công",
|
||||
"Upstream Request ID": "ID yêu cầu thượng nguồn",
|
||||
"Upstream Response": "Upstream feedback",
|
||||
"Upstream Response (billing-usage-anthropic-estimated)": "Phản hồi upstream (billing-usage-anthropic-estimated)",
|
||||
"Upstream Response (billing-usage-anthropic)": "Phản hồi upstream (billing-usage-anthropic)",
|
||||
"Upstream Response (billing-usage-gemini-estimated)": "Phản hồi upstream (billing-usage-gemini-estimated)",
|
||||
"Upstream Response (billing-usage-gemini)": "Phản hồi upstream (billing-usage-gemini)",
|
||||
"Upstream Response (billing-usage-openai-estimated)": "Phản hồi upstream (billing-usage-openai-estimated)",
|
||||
"Upstream Response (billing-usage-openai)": "Phản hồi upstream (billing-usage-openai)",
|
||||
"upstream services integrated": "dịch vụ thượng nguồn tích hợp",
|
||||
"Upstream Updates": "Cập nhật nguồn",
|
||||
"Upstream URL": "URL upstream",
|
||||
@@ -4819,6 +4871,8 @@
|
||||
"Use authenticator code": "Sử dụng mã xác thực",
|
||||
"Use backup code": "Sử dụng mã dự phòng",
|
||||
"Use disk cache when request body exceeds this size": "Sử dụng bộ nhớ đệm đĩa khi nội dung yêu cầu vượt quá kích thước này",
|
||||
"Use exact client model names, separated by commas. Prefixes and wildcards are not supported.": "Nhập tên model chính xác từ yêu cầu client, ngăn cách bằng dấu phẩy. Không hỗ trợ tiền tố hoặc ký tự đại diện.",
|
||||
"Use exact model names such as gpt-4o, or regex rules prefixed with re: such as re:^gemini-.": "Dùng tên model chính xác như gpt-4o, hoặc quy tắc regex có tiền tố re: như re:^gemini-.",
|
||||
"Use external tools to extend capabilities": "Sử dụng công cụ ngoài để mở rộng khả năng",
|
||||
"Use one available reset credit for this channel. The reset request is sent only after confirmation.": "Sử dụng một lượt đặt lại khả dụng cho kênh này. Yêu cầu chỉ được gửi sau khi xác nhận.",
|
||||
"Use one available reset credit to refresh the current Codex usage windows.": "Sử dụng một lượt đặt lại khả dụng để làm mới các cửa sổ mức dùng Codex hiện tại.",
|
||||
|
||||
+54
@@ -207,6 +207,7 @@
|
||||
"Add rule group": "新增規則組",
|
||||
"Add rules for a user group": "為用戶分組新增規則",
|
||||
"Add selectable group": "新增可選分組",
|
||||
"Add split": "新增分流",
|
||||
"Add subscription": "新增訂閱",
|
||||
"Add tags...": "新增標籤...",
|
||||
"Add tier": "新增檔位",
|
||||
@@ -617,6 +618,7 @@
|
||||
"Billing group = vip (the token has no group, so use the user group)": "收費分組 = vip(令牌沒設定分組,就用用戶自己的分組)",
|
||||
"Billing History": "收費歷史",
|
||||
"Billing Mode": "收費模式",
|
||||
"Billing Path": "收費路徑",
|
||||
"Billing Process": "收費過程",
|
||||
"Billing rule: each call is billed as the token group (falling back to the user group when the token has none). The base ratio always comes from that billing group, not from the user group. To give a user group a special price on another billing group, add an entry in the override matrix.": "收費規則:每次呼叫按令牌分組收費(令牌未設定分組時回退到用戶分組)。基礎倍率始終取該收費分組的倍率,而不是用戶分組的倍率。若要讓某用戶分組在使用其他收費分組時享受特殊價格,請在覆蓋矩陣中添加條目。",
|
||||
"Billing Source": "收費來源",
|
||||
@@ -715,6 +717,7 @@
|
||||
"Caps the response length": "限制回覆長度",
|
||||
"Capture a reusable bundle of models, tags, or endpoints.": "捕捉可重用的模型、標籤或端點捆綁包。",
|
||||
"Card view": "卡片檢視",
|
||||
"Catch-all route must be last for the same incoming path": "同一入口路徑的兜底路由必須放在最後",
|
||||
"Category": "分類",
|
||||
"Category Name": "分類名稱",
|
||||
"Category name is required": "分類名稱不能為空",
|
||||
@@ -867,6 +870,9 @@
|
||||
"Click to view image": "點擊查看圖片",
|
||||
"Client header value": "用戶端請求頭值",
|
||||
"Client ID": "Client ID",
|
||||
"Client model": "客戶端 model",
|
||||
"Client model matching": "客戶端 model 匹配",
|
||||
"Client model matching help": "客戶端 model 匹配幫助",
|
||||
"Client Secret": "Client Secret",
|
||||
"Close": "關閉",
|
||||
"Close dialog": "關閉對話框",
|
||||
@@ -1446,6 +1452,7 @@
|
||||
"Drawing task records": "繪圖任務記錄",
|
||||
"Duplicate": "重複",
|
||||
"Duplicate group names: {{names}}": "存在重複的分組名稱:{{names}}",
|
||||
"Duplicate model in route models": "路由模型中存在重複模型",
|
||||
"Duplicate source model mappings are not allowed": "不允許重複的源模型映射",
|
||||
"Duplicate source model(s): {{models}}": "重複的源模型:{{models}}",
|
||||
"Duration": "耗時",
|
||||
@@ -1460,6 +1467,7 @@
|
||||
"e.g. Basic Plan": "例如:基礎套餐",
|
||||
"e.g. Clean tool parameters to avoid upstream validation errors": "例如:清理工具參數,避免上游校驗錯誤",
|
||||
"e.g. example.com": "例如,example.com",
|
||||
"e.g. gpt-4o, gemini-2.5-flash": "例如 gpt-4o, gemini-2.5-flash",
|
||||
"e.g. llama3.1:8b": "例如 llama3.1:8b",
|
||||
"e.g. My GitLab": "例如:My GitLab",
|
||||
"e.g. my-gitlab": "例如:my-gitlab",
|
||||
@@ -1717,6 +1725,7 @@
|
||||
"Everything configured for this group, in one place.": "該分組的全部設定,一處看全。",
|
||||
"Exact": "精確",
|
||||
"Exact Match": "完全匹配",
|
||||
"Exact match only and case-sensitive. Prefixes, regex, and * wildcards are not supported.": "只做完整精確匹配,區分大小寫;不支援前綴、正則或 * 萬用字元。",
|
||||
"Example": "示例",
|
||||
"Example (all channels):": "示例(全部渠道):",
|
||||
"Example (specific channels):": "示例(指定渠道):",
|
||||
@@ -1903,7 +1912,11 @@
|
||||
"Failed to update user": "更新用戶失敗",
|
||||
"Failure keywords": "失敗關鍵詞",
|
||||
"Fair": "公平",
|
||||
"Fallback": "兜底",
|
||||
"Fallback base URL": "兜底 Base URL",
|
||||
"Fallback for remaining models": "留空匹配剩餘模型",
|
||||
"Fallback must be last": "兜底必須在最後",
|
||||
"Fallback route": "兜底路由",
|
||||
"Fallback tier": "兜底階梯",
|
||||
"FAQ": "常見問答",
|
||||
"FAQ added. Click \"Save Settings\" to apply.": "FAQ 已新增。點擊「儲存設定」以套用。",
|
||||
@@ -1941,6 +1954,7 @@
|
||||
"Fill Related Models": "填入相關模型",
|
||||
"Fill Template": "填入模板",
|
||||
"Fill Templates": "填充模板",
|
||||
"Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "填寫客戶端請求體裡的完整 model 值,例如 gpt-4o 或 gemini-2.5-flash。多個模型用英文逗號分隔。",
|
||||
"Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "僅為使用 OpenAI 格式的 Gemini/Vertex 渠道填充 thoughtSignature",
|
||||
"Filled {{count}} model(s)": "已填充 {{count}} 個模型",
|
||||
"Filled {{count}} related model(s)": "已填充 {{count}} 個關聯模型",
|
||||
@@ -1982,6 +1996,7 @@
|
||||
"First token": "首字",
|
||||
"First/Last Frame to Video": "首尾生影片",
|
||||
"Fix Abilities": "修復渠道一致性",
|
||||
"Fix order": "修復順序",
|
||||
"Fixed abilities: {{success}} succeeded, {{fails}} failed": "渠道一致性修復完成:{{success}} 個成功,{{fails}} 個失敗",
|
||||
"Fixed price": "固定價格",
|
||||
"Fixed price (USD)": "固定價格 (USD)",
|
||||
@@ -2426,16 +2441,21 @@
|
||||
"Leave blank to keep the existing credential": "留空以保留現有憑證",
|
||||
"Leave blank to keep the existing key": "留空以保留現有金鑰",
|
||||
"Leave blank unless rotating the secret": "除非正在輪換金鑰,否則留空",
|
||||
"Leave empty for fallback": "留空作為兜底",
|
||||
"Leave empty for never expires": "留空表示永不失效",
|
||||
"Leave empty only for the final fallback split.": "只有最後一個兜底分流可以留空。",
|
||||
"Leave empty to disable the agreement requirement. Supports Markdown, HTML, or a full URL to redirect users.": "留空以停用協議要求。支援 Markdown、HTML 或用於重新導向用戶的完整 URL。",
|
||||
"Leave empty to disable the privacy policy requirement. Supports Markdown, HTML, or a full URL to redirect users.": "留空以停用隱私政策要求。支援 Markdown、HTML 或用於重新導向用戶的完整 URL。",
|
||||
"Leave empty to disband the tag": "留空以解散標籤",
|
||||
"Leave empty to keep existing key": "留空以保留現有金鑰",
|
||||
"Leave empty to keep unchanged": "留空以保持不變",
|
||||
"Leave empty to match all models": "留空匹配所有模型",
|
||||
"Leave empty to use account email": "留空以使用用戶電郵",
|
||||
"Leave empty to use default": "留空使用預設",
|
||||
"Leave empty to use system temp directory": "留空使用系統臨時目錄",
|
||||
"Leave empty to use username": "留空以使用用戶名",
|
||||
"Leave the final split empty as the fallback for models not matched above.": "將最後一個分流留空作為兜底,用於匹配前面未命中的模型。",
|
||||
"Leave this empty only for the final fallback split; it catches client models not matched above.": "只有最後一個兜底分流可以留空;它會匹配前面未命中的客戶端模型。",
|
||||
"Left to Right": "從左到右",
|
||||
"Legacy Format (JSON Object)": "舊格式(JSON 物件)",
|
||||
"Legacy format must be a JSON object": "舊格式必須是 JSON 物件",
|
||||
@@ -2482,6 +2502,9 @@
|
||||
"Loading...": "載入中...",
|
||||
"Local": "本地",
|
||||
"Local Billing": "本地收費",
|
||||
"Local Estimate (billing-usage-anthropic)": "本地估算(billing-usage-anthropic)",
|
||||
"Local Estimate (billing-usage-gemini)": "本地估算(billing-usage-gemini)",
|
||||
"Local Estimate (billing-usage-openai)": "本地估算(billing-usage-openai)",
|
||||
"Local models": "本地模型",
|
||||
"Locations": "位置",
|
||||
"Locked": "鎖定",
|
||||
@@ -2548,7 +2571,9 @@
|
||||
"Match Value": "匹配值",
|
||||
"Match Value (optional)": "匹配值(可選)",
|
||||
"Matched": "已命中",
|
||||
"Matched models": "匹配模型",
|
||||
"Matched Tier": "命中階梯",
|
||||
"Matches models not claimed by earlier splits.": "匹配前面分流未佔用的模型。",
|
||||
"Matching Rules": "匹配規則",
|
||||
"Max Disk Cache Size (MB)": "磁碟緩存最大總量 (MB)",
|
||||
"Max Entries": "最大條目數",
|
||||
@@ -2662,12 +2687,15 @@
|
||||
"Model ratios reset successfully": "模型比例重置成功",
|
||||
"Model Regex": "模型正則",
|
||||
"Model Regex (one per line)": "模型正則(每行一個)",
|
||||
"Model regex cannot be empty": "模型正則不能為空",
|
||||
"Model scope": "模型範圍",
|
||||
"Model selected": "已選擇模型",
|
||||
"Model Square": "模型廣場",
|
||||
"Model Tags": "模型標籤",
|
||||
"Model to use for testing": "用於測試的模型",
|
||||
"Model to use when testing channel connectivity": "測試渠道連接時使用的模型",
|
||||
"Model Version *": "模型版本 *",
|
||||
"Model-scoped only": "僅模型分流",
|
||||
"model(s) selected out of": "已選模型(共)",
|
||||
"model(s)? This action cannot be undone.": "模型?此操作無法撤銷。",
|
||||
"models": "個模型",
|
||||
@@ -2714,9 +2742,12 @@
|
||||
"Move": "移動",
|
||||
"Move a request header": "移動請求頭",
|
||||
"Move affiliate rewards to your main balance": "將推廣獎勵轉移到您的主餘額",
|
||||
"Move fallback to end": "兜底移到最後",
|
||||
"Move Field": "移動欄位",
|
||||
"Move Header": "移動請求頭",
|
||||
"Move Request Header": "移動請求頭",
|
||||
"Move route down": "下移路由",
|
||||
"Move route up": "上移路由",
|
||||
"Move source field to target field": "把來源欄位移動到目標欄位",
|
||||
"ms": "毫秒",
|
||||
"Multi-key channel: Keys will be": "多金鑰渠道:金鑰將",
|
||||
@@ -3050,6 +3081,7 @@
|
||||
"Only enabled parameters are sent with the request.": "只有啟用的參數會隨請求傳送。",
|
||||
"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 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.": "僅成功的請求計入此限制。",
|
||||
@@ -3088,6 +3120,7 @@
|
||||
"OpenAI Rerank": "OpenAI 重排序",
|
||||
"OpenAI Responses": "OpenAI 回應",
|
||||
"OpenAI Responses Compact": "OpenAI 回應壓縮",
|
||||
"OpenAI Responses to Gemini Generate Content": "OpenAI Responses 轉 Gemini Generate Content",
|
||||
"OpenAI Responses to OpenAI Chat": "OpenAI Responses 到 OpenAI Chat",
|
||||
"OpenAI, Anthropic, etc.": "OpenAI、Anthropic 等",
|
||||
"OpenAI, Anthropic, Google, etc.": "OpenAI、Anthropic、Google 等",
|
||||
@@ -3835,9 +3868,15 @@
|
||||
"Route": "路由",
|
||||
"Route active": "路由已啟用",
|
||||
"Route Description": "路由描述",
|
||||
"Route group": "路由組",
|
||||
"Route is required": "路由為必填項",
|
||||
"Route models must be unique for the same incoming path": "同一入口路徑下的路由模型必須唯一",
|
||||
"Route, auth, and balance check in one place": "路由、認證和餘額檢查集中展示",
|
||||
"Routes": "路由",
|
||||
"Routes with the same incoming path are matched by model. Leave the model scope empty only for the final fallback route.": "同一入口路徑的路由按模型匹配。僅最後一個兜底路由可留空模型範圍。",
|
||||
"Routes with the same incoming path are split by client model rules. Unmatched requests use the final fallback.": "同一入口路徑按客戶端 model 規則分流;未命中的請求走最後的兜底。",
|
||||
"Routes with the same incoming path are split by exact client model. Unmatched requests use the final fallback.": "同一入口路徑按客戶端 model 精確分流;未命中的請求走最後的兜底。",
|
||||
"Routes with the same incoming path match exact client model names. Separate multiple models with commas, and leave only the final fallback empty.": "同一入口路徑按客戶端請求中的精確模型名匹配。多個模型用英文逗號分隔,只有最後的兜底可留空。",
|
||||
"Routing & Overrides": "路由與覆蓋",
|
||||
"Routing Reliability": "路由可靠性",
|
||||
"Routing Strategy": "路由策略",
|
||||
@@ -3862,6 +3901,7 @@
|
||||
"Rules": "規則",
|
||||
"Rules JSON": "規則 JSON",
|
||||
"Rules JSON must be an array": "規則 JSON 必須是陣列",
|
||||
"Rules match the original model value from the client request body.": "規則匹配客戶端請求體裡的原始 model 值。",
|
||||
"Run GC": "執行 GC",
|
||||
"Run tests for the selected models": "執行所選模型的測試",
|
||||
"running": "執行中",
|
||||
@@ -4057,6 +4097,7 @@
|
||||
"Sensitive Words": "敏感詞",
|
||||
"Sent the API key to FluentRead.": "API 金鑰已發送至 FluentRead。",
|
||||
"Separate image/audio prices are enabled.": "已啟用圖像/音頻單獨定價。",
|
||||
"Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "多個規則用英文逗號分隔。正則裡需要逗號時,請切換到 JSON 文字。",
|
||||
"Serve multiple users or teams with billing and quota control.": "為多個用戶或團隊提供收費和配額管理服務。",
|
||||
"Server Address": "伺服器地址",
|
||||
"Server IP": "伺服器 IP",
|
||||
@@ -4470,6 +4511,7 @@
|
||||
"This FAQ entry will be removed from the list.": "此 FAQ 條目將從列表中移除。",
|
||||
"This feature is experimental. Configuration format and behavior may change.": "此功能為實驗性功能。設定格式和行為可能會發生變化。",
|
||||
"This feature requires server-side WeChat configuration": "此功能需要伺服器端微信設定",
|
||||
"This field does not support wildcards or regular expressions.": "這裡不支援萬用字元或正則表達式。",
|
||||
"This historical record predates audit-info tracking and cannot be backfilled. The current instance already records server IP, callback IP, payment method, and system version for new top-ups going forward.": "該條歷史記錄缺少審計欄位。目前版本已支援記錄伺服器 IP、Callback IP、支付方式與系統版本等審計資訊;這些欄位僅會寫入後續新產生的記錄,歷史記錄無法自動補齊。",
|
||||
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "建立訂單時會把這個標識提交給支付後端。支付寶填 alipay,微信填 wxpay,Stripe 填 stripe。自訂值必須是支付服務支援的標識。",
|
||||
"This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.": "該實例正在使用自動主機名稱。請設定穩定且唯一的 NODE_NAME,以便進行多實例管理。",
|
||||
@@ -4536,9 +4578,13 @@
|
||||
"Timing": "耗時",
|
||||
"Tip": "提示",
|
||||
"to access this resource.": "存取此資源。",
|
||||
"To Anthropic Messages": "轉 Anthropic Messages",
|
||||
"to confirm": "以確認",
|
||||
"To Gemini Generate Content": "轉 Gemini Generate Content",
|
||||
"To Lower": "轉小寫",
|
||||
"To Lowercase": "轉小寫",
|
||||
"To OpenAI Chat": "轉 OpenAI Chat",
|
||||
"To OpenAI Responses": "轉 OpenAI Responses",
|
||||
"to override billing when a user in one group uses a token of another group.": "當一個分組中的用戶使用另一個分組的令牌時,用於覆蓋收費。",
|
||||
"to the Models list so users can use them before the mapping sends traffic upstream.": "到模型列表,以便用戶在映射將流量發送到上游之前可以使用它們。",
|
||||
"To Upper": "轉大寫",
|
||||
@@ -4786,6 +4832,12 @@
|
||||
"Upstream ratios fetched successfully": "上游比率獲取成功",
|
||||
"Upstream Request ID": "上游請求 ID",
|
||||
"Upstream Response": "上游返回",
|
||||
"Upstream Response (billing-usage-anthropic-estimated)": "上游返回(billing-usage-anthropic-estimated)",
|
||||
"Upstream Response (billing-usage-anthropic)": "上游返回(billing-usage-anthropic)",
|
||||
"Upstream Response (billing-usage-gemini-estimated)": "上游返回(billing-usage-gemini-estimated)",
|
||||
"Upstream Response (billing-usage-gemini)": "上游返回(billing-usage-gemini)",
|
||||
"Upstream Response (billing-usage-openai-estimated)": "上游返回(billing-usage-openai-estimated)",
|
||||
"Upstream Response (billing-usage-openai)": "上游返回(billing-usage-openai)",
|
||||
"upstream services integrated": "上游服務適配",
|
||||
"Upstream Updates": "上游更新",
|
||||
"Upstream URL": "上游 URL",
|
||||
@@ -4819,6 +4871,8 @@
|
||||
"Use authenticator code": "使用驗證器代碼",
|
||||
"Use backup code": "使用備用代碼",
|
||||
"Use disk cache when request body exceeds this size": "請求體超過此大小時使用磁碟緩存",
|
||||
"Use exact client model names, separated by commas. Prefixes and wildcards are not supported.": "填寫客戶端請求裡的精確 model 名,多個用英文逗號分隔。不支援前綴或萬用字元。",
|
||||
"Use exact model names such as gpt-4o, or regex rules prefixed with re: such as re:^gemini-.": "可以填寫 gpt-4o 這類精確模型名,也可以填寫 re:^gemini- 這類以 re: 開頭的正則規則。",
|
||||
"Use external tools to extend capabilities": "透過外部工具擴展能力",
|
||||
"Use one available reset credit for this channel. The reset request is sent only after confirmation.": "將為目前渠道使用 1 次可用重置次數。只有確認後才會發送重置請求。",
|
||||
"Use one available reset credit to refresh the current Codex usage windows.": "使用 1 次可用重置次數,重新整理目前 Codex 用量窗口。",
|
||||
|
||||
Vendored
+54
@@ -207,6 +207,7 @@
|
||||
"Add rule group": "新增规则组",
|
||||
"Add rules for a user group": "为用户分组添加规则",
|
||||
"Add selectable group": "添加可选分组",
|
||||
"Add split": "添加分流",
|
||||
"Add subscription": "新增订阅",
|
||||
"Add tags...": "添加标签...",
|
||||
"Add tier": "新增档位",
|
||||
@@ -617,6 +618,7 @@
|
||||
"Billing group = vip (the token has no group, so use the user group)": "计费分组 = vip(令牌没设置分组,就用用户自己的分组)",
|
||||
"Billing History": "计费历史",
|
||||
"Billing Mode": "计费模式",
|
||||
"Billing Path": "计费路径",
|
||||
"Billing Process": "计费过程",
|
||||
"Billing rule: each call is billed as the token group (falling back to the user group when the token has none). The base ratio always comes from that billing group, not from the user group. To give a user group a special price on another billing group, add an entry in the override matrix.": "计费规则:每次调用按令牌分组计费(令牌未设置分组时回退到用户分组)。基础倍率始终取该计费分组的倍率,而不是用户分组的倍率。若要让某用户分组在使用其他计费分组时享受特殊价格,请在覆盖矩阵中添加条目。",
|
||||
"Billing Source": "计费来源",
|
||||
@@ -715,6 +717,7 @@
|
||||
"Caps the response length": "限制回复长度",
|
||||
"Capture a reusable bundle of models, tags, or endpoints.": "捕获可重用的模型、标签或端点捆绑包。",
|
||||
"Card view": "卡片视图",
|
||||
"Catch-all route must be last for the same incoming path": "同一入口路径的兜底路由必须放在最后",
|
||||
"Category": "分类",
|
||||
"Category Name": "分类名称",
|
||||
"Category name is required": "分类名称不能为空",
|
||||
@@ -867,6 +870,9 @@
|
||||
"Click to view image": "点击查看图片",
|
||||
"Client header value": "客户端请求头值",
|
||||
"Client ID": "Client ID",
|
||||
"Client model": "客户端 model",
|
||||
"Client model matching": "客户端 model 匹配",
|
||||
"Client model matching help": "客户端 model 匹配帮助",
|
||||
"Client Secret": "Client Secret",
|
||||
"Close": "关闭",
|
||||
"Close dialog": "关闭对话框",
|
||||
@@ -1446,6 +1452,7 @@
|
||||
"Drawing task records": "绘图任务记录",
|
||||
"Duplicate": "重复",
|
||||
"Duplicate group names: {{names}}": "存在重复的分组名称:{{names}}",
|
||||
"Duplicate model in route models": "路由模型中存在重复模型",
|
||||
"Duplicate source model mappings are not allowed": "不允许重复的源模型映射",
|
||||
"Duplicate source model(s): {{models}}": "重复的源模型:{{models}}",
|
||||
"Duration": "耗时",
|
||||
@@ -1460,6 +1467,7 @@
|
||||
"e.g. Basic Plan": "例如:基础套餐",
|
||||
"e.g. Clean tool parameters to avoid upstream validation errors": "例如:清理工具参数,避免上游校验错误",
|
||||
"e.g. example.com": "例如,example.com",
|
||||
"e.g. gpt-4o, gemini-2.5-flash": "例如 gpt-4o, gemini-2.5-flash",
|
||||
"e.g. llama3.1:8b": "例如 llama3.1:8b",
|
||||
"e.g. My GitLab": "例如:My GitLab",
|
||||
"e.g. my-gitlab": "例如:my-gitlab",
|
||||
@@ -1717,6 +1725,7 @@
|
||||
"Everything configured for this group, in one place.": "该分组的全部配置,一处看全。",
|
||||
"Exact": "精确",
|
||||
"Exact Match": "完全匹配",
|
||||
"Exact match only and case-sensitive. Prefixes, regex, and * wildcards are not supported.": "只做完整精确匹配,区分大小写;不支持前缀、正则或 * 通配。",
|
||||
"Example": "示例",
|
||||
"Example (all channels):": "示例(全部渠道):",
|
||||
"Example (specific channels):": "示例(指定渠道):",
|
||||
@@ -1903,7 +1912,11 @@
|
||||
"Failed to update user": "更新用户失败",
|
||||
"Failure keywords": "失败关键词",
|
||||
"Fair": "公平",
|
||||
"Fallback": "兜底",
|
||||
"Fallback base URL": "兜底 Base URL",
|
||||
"Fallback for remaining models": "留空匹配剩余模型",
|
||||
"Fallback must be last": "兜底必须在最后",
|
||||
"Fallback route": "兜底路由",
|
||||
"Fallback tier": "兜底阶梯",
|
||||
"FAQ": "常见问答",
|
||||
"FAQ added. Click \"Save Settings\" to apply.": "FAQ 已添加。点击 \"保存设置\" 以应用。",
|
||||
@@ -1941,6 +1954,7 @@
|
||||
"Fill Related Models": "填入相关模型",
|
||||
"Fill Template": "填入模板",
|
||||
"Fill Templates": "填充模板",
|
||||
"Fill the complete model value from the client request body, for example gpt-4o or gemini-2.5-flash. Separate multiple models with commas.": "填写客户端请求体里的完整 model 值,例如 gpt-4o 或 gemini-2.5-flash。多个模型用英文逗号分隔。",
|
||||
"Fill thoughtSignature only for Gemini/Vertex channels using the OpenAI format": "仅为使用 OpenAI 格式的 Gemini/Vertex 渠道填充 thoughtSignature",
|
||||
"Filled {{count}} model(s)": "已填充 {{count}} 个模型",
|
||||
"Filled {{count}} related model(s)": "已填充 {{count}} 个关联模型",
|
||||
@@ -1982,6 +1996,7 @@
|
||||
"First token": "首字",
|
||||
"First/Last Frame to Video": "首尾生视频",
|
||||
"Fix Abilities": "修复渠道一致性",
|
||||
"Fix order": "修复顺序",
|
||||
"Fixed abilities: {{success}} succeeded, {{fails}} failed": "渠道一致性修复完成:{{success}} 个成功,{{fails}} 个失败",
|
||||
"Fixed price": "固定价格",
|
||||
"Fixed price (USD)": "固定价格 (USD)",
|
||||
@@ -2426,16 +2441,21 @@
|
||||
"Leave blank to keep the existing credential": "留空以保留现有凭证",
|
||||
"Leave blank to keep the existing key": "留空以保留现有密钥",
|
||||
"Leave blank unless rotating the secret": "除非正在轮换密钥,否则留空",
|
||||
"Leave empty for fallback": "留空作为兜底",
|
||||
"Leave empty for never expires": "留空表示永不失效",
|
||||
"Leave empty only for the final fallback split.": "只有最后一个兜底分流可以留空。",
|
||||
"Leave empty to disable the agreement requirement. Supports Markdown, HTML, or a full URL to redirect users.": "留空以禁用协议要求。支持 Markdown、HTML 或用于重定向用户的完整 URL。",
|
||||
"Leave empty to disable the privacy policy requirement. Supports Markdown, HTML, or a full URL to redirect users.": "留空以禁用隐私政策要求。支持 Markdown、HTML 或用于重定向用户的完整 URL。",
|
||||
"Leave empty to disband the tag": "留空以解散标签",
|
||||
"Leave empty to keep existing key": "留空以保留现有密钥",
|
||||
"Leave empty to keep unchanged": "留空以保持不变",
|
||||
"Leave empty to match all models": "留空匹配所有模型",
|
||||
"Leave empty to use account email": "留空以使用账户邮箱",
|
||||
"Leave empty to use default": "留空使用默认",
|
||||
"Leave empty to use system temp directory": "留空使用系统临时目录",
|
||||
"Leave empty to use username": "留空以使用用户名",
|
||||
"Leave the final split empty as the fallback for models not matched above.": "将最后一个分流留空作为兜底,用于匹配前面未命中的模型。",
|
||||
"Leave this empty only for the final fallback split; it catches client models not matched above.": "只有最后一个兜底分流可以留空;它会匹配前面未命中的客户端模型。",
|
||||
"Left to Right": "从左到右",
|
||||
"Legacy Format (JSON Object)": "旧格式(JSON 对象)",
|
||||
"Legacy format must be a JSON object": "旧格式必须是 JSON 对象",
|
||||
@@ -2482,6 +2502,9 @@
|
||||
"Loading...": "加载中...",
|
||||
"Local": "本地",
|
||||
"Local Billing": "本地计费",
|
||||
"Local Estimate (billing-usage-anthropic)": "本地估算(billing-usage-anthropic)",
|
||||
"Local Estimate (billing-usage-gemini)": "本地估算(billing-usage-gemini)",
|
||||
"Local Estimate (billing-usage-openai)": "本地估算(billing-usage-openai)",
|
||||
"Local models": "本地模型",
|
||||
"Locations": "位置",
|
||||
"Locked": "锁定",
|
||||
@@ -2548,7 +2571,9 @@
|
||||
"Match Value": "匹配值",
|
||||
"Match Value (optional)": "匹配值(可选)",
|
||||
"Matched": "已命中",
|
||||
"Matched models": "匹配模型",
|
||||
"Matched Tier": "命中阶梯",
|
||||
"Matches models not claimed by earlier splits.": "匹配前面分流未占用的模型。",
|
||||
"Matching Rules": "匹配规则",
|
||||
"Max Disk Cache Size (MB)": "磁盘缓存最大总量 (MB)",
|
||||
"Max Entries": "最大条目数",
|
||||
@@ -2662,12 +2687,15 @@
|
||||
"Model ratios reset successfully": "模型比例重置成功",
|
||||
"Model Regex": "模型正则",
|
||||
"Model Regex (one per line)": "模型正则(每行一个)",
|
||||
"Model regex cannot be empty": "模型正则不能为空",
|
||||
"Model scope": "模型范围",
|
||||
"Model selected": "已选择模型",
|
||||
"Model Square": "模型广场",
|
||||
"Model Tags": "模型标签",
|
||||
"Model to use for testing": "用于测试的模型",
|
||||
"Model to use when testing channel connectivity": "测试渠道连接时使用的模型",
|
||||
"Model Version *": "模型版本 *",
|
||||
"Model-scoped only": "仅模型分流",
|
||||
"model(s) selected out of": "已选模型(共)",
|
||||
"model(s)? This action cannot be undone.": "模型?此操作无法撤销。",
|
||||
"models": "个模型",
|
||||
@@ -2714,9 +2742,12 @@
|
||||
"Move": "移动",
|
||||
"Move a request header": "移动请求头",
|
||||
"Move affiliate rewards to your main balance": "将推广奖励转移到您的主余额",
|
||||
"Move fallback to end": "兜底移到最后",
|
||||
"Move Field": "移动字段",
|
||||
"Move Header": "移动请求头",
|
||||
"Move Request Header": "移动请求头",
|
||||
"Move route down": "下移路由",
|
||||
"Move route up": "上移路由",
|
||||
"Move source field to target field": "把来源字段移动到目标字段",
|
||||
"ms": "毫秒",
|
||||
"Multi-key channel: Keys will be": "多密钥渠道:密钥将",
|
||||
@@ -3050,6 +3081,7 @@
|
||||
"Only enabled parameters are sent with the request.": "只有启用的参数会随请求发送。",
|
||||
"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 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.": "仅成功的请求计入此限制。",
|
||||
@@ -3088,6 +3120,7 @@
|
||||
"OpenAI Rerank": "OpenAI 重排序",
|
||||
"OpenAI Responses": "OpenAI 响应",
|
||||
"OpenAI Responses Compact": "OpenAI 响应压缩",
|
||||
"OpenAI Responses to Gemini Generate Content": "OpenAI Responses 转 Gemini Generate Content",
|
||||
"OpenAI Responses to OpenAI Chat": "OpenAI Responses 到 OpenAI Chat",
|
||||
"OpenAI, Anthropic, etc.": "OpenAI、Anthropic 等",
|
||||
"OpenAI, Anthropic, Google, etc.": "OpenAI、Anthropic、Google 等",
|
||||
@@ -3835,9 +3868,15 @@
|
||||
"Route": "路由",
|
||||
"Route active": "路由已启用",
|
||||
"Route Description": "路由描述",
|
||||
"Route group": "路由组",
|
||||
"Route is required": "路由为必填项",
|
||||
"Route models must be unique for the same incoming path": "同一入口路径下的路由模型必须唯一",
|
||||
"Route, auth, and balance check in one place": "路由、认证和余额检查集中展示",
|
||||
"Routes": "路由",
|
||||
"Routes with the same incoming path are matched by model. Leave the model scope empty only for the final fallback route.": "同一入口路径的路由按模型匹配。仅最后一个兜底路由可留空模型范围。",
|
||||
"Routes with the same incoming path are split by client model rules. Unmatched requests use the final fallback.": "同一入口路径按客户端 model 规则分流;未命中的请求走最后的兜底。",
|
||||
"Routes with the same incoming path are split by exact client model. Unmatched requests use the final fallback.": "同一入口路径按客户端 model 精确分流;未命中的请求走最后的兜底。",
|
||||
"Routes with the same incoming path match exact client model names. Separate multiple models with commas, and leave only the final fallback empty.": "同一入口路径按客户端请求中的精确模型名匹配。多个模型用英文逗号分隔,只有最后的兜底可留空。",
|
||||
"Routing & Overrides": "路由与覆盖",
|
||||
"Routing Reliability": "路由可靠性",
|
||||
"Routing Strategy": "路由策略",
|
||||
@@ -3862,6 +3901,7 @@
|
||||
"Rules": "规则",
|
||||
"Rules JSON": "规则 JSON",
|
||||
"Rules JSON must be an array": "规则 JSON 必须是数组",
|
||||
"Rules match the original model value from the client request body.": "规则匹配客户端请求体里的原始 model 值。",
|
||||
"Run GC": "执行 GC",
|
||||
"Run tests for the selected models": "运行所选模型的测试",
|
||||
"running": "运行中",
|
||||
@@ -4057,6 +4097,7 @@
|
||||
"Sensitive Words": "敏感词",
|
||||
"Sent the API key to FluentRead.": "API 密钥已发送至 FluentRead。",
|
||||
"Separate image/audio prices are enabled.": "已启用图像/音频单独定价。",
|
||||
"Separate multiple rules with English commas. For regex patterns that need commas, switch to JSON Text.": "多个规则用英文逗号分隔。正则里需要逗号时,请切换到 JSON 文本。",
|
||||
"Serve multiple users or teams with billing and quota control.": "为多个用户或团队提供计费和配额管理服务。",
|
||||
"Server Address": "服务器地址",
|
||||
"Server IP": "服务器 IP",
|
||||
@@ -4470,6 +4511,7 @@
|
||||
"This FAQ entry will be removed from the list.": "此 FAQ 条目将从列表中移除。",
|
||||
"This feature is experimental. Configuration format and behavior may change.": "此功能为实验性功能。配置格式和行为可能会发生变化。",
|
||||
"This feature requires server-side WeChat configuration": "此功能需要服务器端微信配置",
|
||||
"This field does not support wildcards or regular expressions.": "这里不支持通配符或正则表达式。",
|
||||
"This historical record predates audit-info tracking and cannot be backfilled. The current instance already records server IP, callback IP, payment method, and system version for new top-ups going forward.": "该条历史记录缺少审计字段。当前版本已支持记录服务器 IP、回调 IP、支付方式与系统版本等审计信息;这些字段仅会写入后续新产生的记录,历史记录无法自动补齐。",
|
||||
"This identifier is sent to the payment backend when creating an order. Use alipay for Alipay, wxpay for WeChat Pay, stripe for Stripe. Custom values must be supported by your payment provider.": "创建订单时会把这个标识提交给支付后端。支付宝填 alipay,微信填 wxpay,Stripe 填 stripe。自定义值必须是支付服务支持的标识。",
|
||||
"This instance is using an automatic hostname. Set NODE_NAME to a stable unique value for multi-instance management.": "该实例正在使用自动主机名。请设置稳定且唯一的 NODE_NAME,以便进行多实例管理。",
|
||||
@@ -4536,9 +4578,13 @@
|
||||
"Timing": "耗时",
|
||||
"Tip": "提示",
|
||||
"to access this resource.": "访问此资源。",
|
||||
"To Anthropic Messages": "转 Anthropic Messages",
|
||||
"to confirm": "以确认",
|
||||
"To Gemini Generate Content": "转 Gemini Generate Content",
|
||||
"To Lower": "转小写",
|
||||
"To Lowercase": "转小写",
|
||||
"To OpenAI Chat": "转 OpenAI Chat",
|
||||
"To OpenAI Responses": "转 OpenAI Responses",
|
||||
"to override billing when a user in one group uses a token of another group.": "当一个分组中的用户使用另一个分组的令牌时,用于覆盖计费。",
|
||||
"to the Models list so users can use them before the mapping sends traffic upstream.": "到模型列表,以便用户在映射将流量发送到上游之前可以使用它们。",
|
||||
"To Upper": "转大写",
|
||||
@@ -4786,6 +4832,12 @@
|
||||
"Upstream ratios fetched successfully": "上游比率获取成功",
|
||||
"Upstream Request ID": "上游请求 ID",
|
||||
"Upstream Response": "上游返回",
|
||||
"Upstream Response (billing-usage-anthropic-estimated)": "上游返回(billing-usage-anthropic-estimated)",
|
||||
"Upstream Response (billing-usage-anthropic)": "上游返回(billing-usage-anthropic)",
|
||||
"Upstream Response (billing-usage-gemini-estimated)": "上游返回(billing-usage-gemini-estimated)",
|
||||
"Upstream Response (billing-usage-gemini)": "上游返回(billing-usage-gemini)",
|
||||
"Upstream Response (billing-usage-openai-estimated)": "上游返回(billing-usage-openai-estimated)",
|
||||
"Upstream Response (billing-usage-openai)": "上游返回(billing-usage-openai)",
|
||||
"upstream services integrated": "上游服务适配",
|
||||
"Upstream Updates": "上游更新",
|
||||
"Upstream URL": "上游 URL",
|
||||
@@ -4819,6 +4871,8 @@
|
||||
"Use authenticator code": "使用验证器代码",
|
||||
"Use backup code": "使用备用代码",
|
||||
"Use disk cache when request body exceeds this size": "请求体超过此大小时使用磁盘缓存",
|
||||
"Use exact client model names, separated by commas. Prefixes and wildcards are not supported.": "填写客户端请求里的精确 model 名,多个用英文逗号分隔。不支持前缀或通配符。",
|
||||
"Use exact model names such as gpt-4o, or regex rules prefixed with re: such as re:^gemini-.": "可以填写 gpt-4o 这类精确模型名,也可以填写 re:^gemini- 这类以 re: 开头的正则规则。",
|
||||
"Use external tools to extend capabilities": "通过外部工具扩展能力",
|
||||
"Use one available reset credit for this channel. The reset request is sent only after confirmation.": "将为当前渠道使用 1 次可用重置次数。只有确认后才会发送重置请求。",
|
||||
"Use one available reset credit to refresh the current Codex usage windows.": "使用 1 次可用重置次数,刷新当前 Codex 用量窗口。",
|
||||
|
||||
Reference in New Issue
Block a user