Commit 91ab664c by CaIon

feat: add routing reliability management

parent dfcb74b5
...@@ -85,6 +85,7 @@ Do NOT directly import or call `encoding/json` in business code. `json.RawMessag ...@@ -85,6 +85,7 @@ Do NOT directly import or call `encoding/json` in business code. `json.RawMessag
- Use `common.UsingPostgreSQL`, `common.UsingSQLite`, and `common.UsingMySQL` flags for DB-specific branches. - Use `common.UsingPostgreSQL`, `common.UsingSQLite`, and `common.UsingMySQL` flags for DB-specific branches.
- Do not use database-specific features without cross-DB fallback, including MySQL-only functions, PostgreSQL-only operators, SQLite-unsupported `ALTER COLUMN`, or database-specific JSON column types without a `TEXT` fallback. - Do not use database-specific features without cross-DB fallback, including MySQL-only functions, PostgreSQL-only operators, SQLite-unsupported `ALTER COLUMN`, or database-specific JSON column types without a `TEXT` fallback.
- Migrations must work on all three databases. For SQLite, use `ALTER TABLE ... ADD COLUMN` instead of `ALTER COLUMN` (see `model/main.go` for patterns). - Migrations must work on all three databases. For SQLite, use `ALTER TABLE ... ADD COLUMN` instead of `ALTER COLUMN` (see `model/main.go` for patterns).
- Avoid GORM boolean default tags such as `gorm:"default:true"` when the default is a business rule already enforced by code. MySQL and PostgreSQL can normalize boolean defaults differently, causing GORM `AutoMigrate` to repeatedly issue `ALTER TABLE` on restart. Prefer setting these defaults in request/model normalization, hooks, constructors, or service logic; do not replace `default:true` with `default:1` unless the behavior is verified across SQLite, MySQL, and PostgreSQL.
**Relay and provider behavior:** **Relay and provider behavior:**
......
...@@ -85,6 +85,7 @@ Do NOT directly import or call `encoding/json` in business code. `json.RawMessag ...@@ -85,6 +85,7 @@ Do NOT directly import or call `encoding/json` in business code. `json.RawMessag
- Use `common.UsingPostgreSQL`, `common.UsingSQLite`, and `common.UsingMySQL` flags for DB-specific branches. - Use `common.UsingPostgreSQL`, `common.UsingSQLite`, and `common.UsingMySQL` flags for DB-specific branches.
- Do not use database-specific features without cross-DB fallback, including MySQL-only functions, PostgreSQL-only operators, SQLite-unsupported `ALTER COLUMN`, or database-specific JSON column types without a `TEXT` fallback. - Do not use database-specific features without cross-DB fallback, including MySQL-only functions, PostgreSQL-only operators, SQLite-unsupported `ALTER COLUMN`, or database-specific JSON column types without a `TEXT` fallback.
- Migrations must work on all three databases. For SQLite, use `ALTER TABLE ... ADD COLUMN` instead of `ALTER COLUMN` (see `model/main.go` for patterns). - Migrations must work on all three databases. For SQLite, use `ALTER TABLE ... ADD COLUMN` instead of `ALTER COLUMN` (see `model/main.go` for patterns).
- Avoid GORM boolean default tags such as `gorm:"default:true"` when the default is a business rule already enforced by code. MySQL and PostgreSQL can normalize boolean defaults differently, causing GORM `AutoMigrate` to repeatedly issue `ALTER TABLE` on restart. Prefer setting these defaults in request/model normalization, hooks, constructors, or service logic; do not replace `default:true` with `default:1` unless the behavior is verified across SQLite, MySQL, and PostgreSQL.
**Relay and provider behavior:** **Relay and provider behavior:**
......
...@@ -89,6 +89,12 @@ func buildChannelListQuery(group string, statusFilter int, typeFilter int) *gorm ...@@ -89,6 +89,12 @@ func buildChannelListQuery(group string, statusFilter int, typeFilter int) *gorm
return query return query
} }
func GetChannelOps(c *gin.Context) {
common.ApiSuccess(c, gin.H{
"retry_times": common.RetryTimes,
})
}
func GetAllChannels(c *gin.Context) { func GetAllChannels(c *gin.Context) {
pageInfo := common.GetPageQuery(c) pageInfo := common.GetPageQuery(c)
channelData := make([]*model.Channel, 0) channelData := make([]*model.Channel, 0)
......
...@@ -232,6 +232,7 @@ func SetApiRouter(router *gin.Engine) { ...@@ -232,6 +232,7 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.GET("/search", controller.SearchChannels) channelRoute.GET("/search", controller.SearchChannels)
channelRoute.GET("/models", controller.ChannelListModels) channelRoute.GET("/models", controller.ChannelListModels)
channelRoute.GET("/models_enabled", controller.EnabledListModels) channelRoute.GET("/models_enabled", controller.EnabledListModels)
channelRoute.GET("/ops", controller.GetChannelOps)
channelRoute.GET("/:id", controller.GetChannel) channelRoute.GET("/:id", controller.GetChannel)
channelRoute.POST("/:id/key", middleware.RootAuth(), middleware.CriticalRateLimit(), middleware.DisableCache(), middleware.SecureVerificationRequired(), controller.GetChannelKey) channelRoute.POST("/:id/key", middleware.RootAuth(), middleware.CriticalRateLimit(), middleware.DisableCache(), middleware.SecureVerificationRequired(), controller.GetChannelKey)
channelRoute.GET("/test", controller.TestAllChannels) channelRoute.GET("/test", controller.TestAllChannels)
......
...@@ -24,6 +24,7 @@ import type { ...@@ -24,6 +24,7 @@ import type {
BatchSetTagParams, BatchSetTagParams,
Channel, Channel,
ChannelBalanceResponse, ChannelBalanceResponse,
ChannelOpsResponse,
ChannelTestResponse, ChannelTestResponse,
CopyChannelParams, CopyChannelParams,
CopyChannelResponse, CopyChannelResponse,
...@@ -104,6 +105,14 @@ export async function getChannel(id: number): Promise<GetChannelResponse> { ...@@ -104,6 +105,14 @@ export async function getChannel(id: number): Promise<GetChannelResponse> {
} }
/** /**
* Get channel operations summary for administrators
*/
export async function getChannelOps(): Promise<ChannelOpsResponse> {
const res = await api.get('/api/channel/ops', channelActionConfig())
return res.data
}
/**
* Create new channel(s) * Create new channel(s)
* Supports single, batch, and multi-key modes * Supports single, batch, and multi-key modes
*/ */
......
...@@ -17,7 +17,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,7 +17,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Link } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { Settings2 } from 'lucide-react'
import { SectionPageLayout } from '@/components/layout' import { SectionPageLayout } from '@/components/layout'
import { Badge } from '@/components/ui/badge'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { ROLE } from '@/lib/roles'
import { useAuthStore } from '@/stores/auth-store'
import { getChannelOps } from './api'
import { ChannelsDialogs } from './components/channels-dialogs' import { ChannelsDialogs } from './components/channels-dialogs'
import { ChannelsPrimaryButtons } from './components/channels-primary-buttons' import { ChannelsPrimaryButtons } from './components/channels-primary-buttons'
import { ChannelsProvider } from './components/channels-provider' import { ChannelsProvider } from './components/channels-provider'
...@@ -25,10 +37,62 @@ import { ChannelsTable } from './components/channels-table' ...@@ -25,10 +37,62 @@ import { ChannelsTable } from './components/channels-table'
export function Channels() { export function Channels() {
const { t } = useTranslation() const { t } = useTranslation()
const isRoot = useAuthStore(
(state) => state.auth.user?.role === ROLE.SUPER_ADMIN
)
const channelOpsQuery = useQuery({
queryKey: ['channel-ops'],
queryFn: getChannelOps,
retry: false,
staleTime: 5 * 60 * 1000,
})
const retryTimes = channelOpsQuery.data?.data?.retry_times
const retryLabel =
typeof retryTimes === 'number'
? `${t('Max Retries')}: ${retryTimes}`
: null
let retryBadge = null
if (retryLabel) {
retryBadge = isRoot ? (
<Tooltip>
<TooltipTrigger
render={
<Badge
variant='outline'
className='shrink-0 cursor-pointer'
aria-label={t('Retry Settings')}
render={
<Link
to='/system-settings/models/$section'
params={{ section: 'routing-reliability' }}
/>
}
/>
}
>
<span>{retryLabel}</span>
<Settings2 data-icon='inline-end' />
</TooltipTrigger>
<TooltipContent>
<p>{t('Retry Settings')}</p>
</TooltipContent>
</Tooltip>
) : (
<Badge variant='outline' className='shrink-0'>
{retryLabel}
</Badge>
)
}
return ( return (
<ChannelsProvider> <ChannelsProvider>
<SectionPageLayout fixedContent> <SectionPageLayout fixedContent>
<SectionPageLayout.Title>{t('Channels')}</SectionPageLayout.Title> <SectionPageLayout.Title>
<span className='flex min-w-0 items-center gap-2'>
<span className='truncate'>{t('Channels')}</span>
{retryBadge}
</span>
</SectionPageLayout.Title>
<SectionPageLayout.Actions> <SectionPageLayout.Actions>
<ChannelsPrimaryButtons /> <ChannelsPrimaryButtons />
</SectionPageLayout.Actions> </SectionPageLayout.Actions>
......
...@@ -167,6 +167,14 @@ export interface GetChannelResponse { ...@@ -167,6 +167,14 @@ export interface GetChannelResponse {
data?: Channel data?: Channel
} }
export interface ChannelOpsResponse {
success: boolean
message?: string
data?: {
retry_times: number
}
}
export interface ChannelTestResponse { export interface ChannelTestResponse {
success: boolean success: boolean
message?: string message?: string
......
...@@ -200,6 +200,16 @@ export function ModelMutateDrawer({ ...@@ -200,6 +200,16 @@ export function ModelMutateDrawer({
'group_ratio_setting.group_special_usable_group': '{}', 'group_ratio_setting.group_special_usable_group': '{}',
'grok.violation_deduction_enabled': false, 'grok.violation_deduction_enabled': false,
'grok.violation_deduction_amount': 0, 'grok.violation_deduction_amount': 0,
RetryTimes: 0,
ChannelDisableThreshold: '',
AutomaticDisableChannelEnabled: false,
AutomaticEnableChannelEnabled: false,
AutomaticDisableKeywords: '',
AutomaticDisableStatusCodes: '401',
AutomaticRetryStatusCodes:
'100-199,300-399,401-407,409-499,500-503,505-523,525-599',
'monitor_setting.auto_test_channel_enabled': false,
'monitor_setting.auto_test_channel_minutes': 10,
'channel_affinity_setting.enabled': false, 'channel_affinity_setting.enabled': false,
'channel_affinity_setting.switch_on_success': true, 'channel_affinity_setting.switch_on_success': true,
'channel_affinity_setting.keep_on_channel_disabled': false, 'channel_affinity_setting.keep_on_channel_disabled': false,
......
...@@ -44,7 +44,7 @@ type SettingsSwitchFieldProps = SettingsSwitchRowProps & { ...@@ -44,7 +44,7 @@ type SettingsSwitchFieldProps = SettingsSwitchRowProps & {
} }
const settingsSwitchRowClassName = const settingsSwitchRowClassName =
'flex min-w-0 flex-row items-center justify-between gap-4 border-b py-2.5 last:border-b-0' 'flex min-w-0 flex-row items-center justify-between gap-4 py-2.5'
export function SettingsFormGrid(props: SettingsFormGridProps) { export function SettingsFormGrid(props: SettingsFormGridProps) {
return ( return (
......
...@@ -339,7 +339,7 @@ export function AnnouncementsSection({ ...@@ -339,7 +339,7 @@ export function AnnouncementsSection({
checked={isEnabled} checked={isEnabled}
onCheckedChange={handleToggleEnabled} onCheckedChange={handleToggleEnabled}
label={t('Enabled')} label={t('Enabled')}
className='border-b-0 py-0' className='py-0'
/> />
</div> </div>
...@@ -529,8 +529,7 @@ export function AnnouncementsSection({ ...@@ -529,8 +529,7 @@ export function AnnouncementsSection({
<FormItem> <FormItem>
<FormLabel>{t('Type')}</FormLabel> <FormLabel>{t('Type')}</FormLabel>
<Select <Select
items={[ items={typeOptions.map((option) => ({
...typeOptions.map((option) => ({
value: option.value, value: option.value,
label: ( label: (
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
...@@ -540,8 +539,7 @@ export function AnnouncementsSection({ ...@@ -540,8 +539,7 @@ export function AnnouncementsSection({
{option.label} {option.label}
</div> </div>
), ),
})), }))}
]}
onValueChange={field.onChange} onValueChange={field.onChange}
value={field.value} value={field.value}
> >
......
...@@ -291,7 +291,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { ...@@ -291,7 +291,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
checked={isEnabled} checked={isEnabled}
onCheckedChange={handleToggleEnabled} onCheckedChange={handleToggleEnabled}
label={t('Enabled')} label={t('Enabled')}
className='border-b-0 py-0' className='py-0'
/> />
</div> </div>
...@@ -475,8 +475,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { ...@@ -475,8 +475,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
<FormItem> <FormItem>
<FormLabel>{t('Badge Color')}</FormLabel> <FormLabel>{t('Badge Color')}</FormLabel>
<Select <Select
items={[ items={colorOptions.map((option) => ({
...colorOptions.map((option) => ({
value: option.value, value: option.value,
label: ( label: (
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
...@@ -486,8 +485,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { ...@@ -486,8 +485,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
{option.label} {option.label}
</div> </div>
), ),
})), }))}
]}
onValueChange={field.onChange} onValueChange={field.onChange}
value={field.value} value={field.value}
> >
......
...@@ -258,7 +258,7 @@ export function FAQSection({ enabled, data }: FAQSectionProps) { ...@@ -258,7 +258,7 @@ export function FAQSection({ enabled, data }: FAQSectionProps) {
checked={isEnabled} checked={isEnabled}
onCheckedChange={handleToggleEnabled} onCheckedChange={handleToggleEnabled}
label={t('Enabled')} label={t('Enabled')}
className='border-b-0 py-0' className='py-0'
/> />
</div> </div>
......
...@@ -267,7 +267,7 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) { ...@@ -267,7 +267,7 @@ export function UptimeKumaSection({ enabled, data }: UptimeKumaSectionProps) {
checked={isEnabled} checked={isEnabled}
onCheckedChange={handleToggleEnabled} onCheckedChange={handleToggleEnabled}
label={t('Enabled')} label={t('Enabled')}
className='border-b-0 py-0' className='py-0'
/> />
</div> </div>
......
...@@ -255,42 +255,42 @@ export function ChannelAffinitySection(props: Props) { ...@@ -255,42 +255,42 @@ export function ChannelAffinitySection(props: Props) {
const updates: { key: string; value: string }[] = [] const updates: { key: string; value: string }[] = []
if (enabled !== props.defaultValues['channel_affinity_setting.enabled']) if (enabled !== props.defaultValues['channel_affinity_setting.enabled'])
updates.push({ {updates.push({
key: 'channel_affinity_setting.enabled', key: 'channel_affinity_setting.enabled',
value: String(enabled), value: String(enabled),
}) })}
if ( if (
switchOnSuccess !== switchOnSuccess !==
props.defaultValues['channel_affinity_setting.switch_on_success'] props.defaultValues['channel_affinity_setting.switch_on_success']
) )
updates.push({ {updates.push({
key: 'channel_affinity_setting.switch_on_success', key: 'channel_affinity_setting.switch_on_success',
value: String(switchOnSuccess), value: String(switchOnSuccess),
}) })}
if ( if (
keepOnChannelDisabled !== keepOnChannelDisabled !==
props.defaultValues['channel_affinity_setting.keep_on_channel_disabled'] props.defaultValues['channel_affinity_setting.keep_on_channel_disabled']
) )
updates.push({ {updates.push({
key: 'channel_affinity_setting.keep_on_channel_disabled', key: 'channel_affinity_setting.keep_on_channel_disabled',
value: String(keepOnChannelDisabled), value: String(keepOnChannelDisabled),
}) })}
if ( if (
maxEntries !== maxEntries !==
props.defaultValues['channel_affinity_setting.max_entries'] props.defaultValues['channel_affinity_setting.max_entries']
) )
updates.push({ {updates.push({
key: 'channel_affinity_setting.max_entries', key: 'channel_affinity_setting.max_entries',
value: String(maxEntries), value: String(maxEntries),
}) })}
if ( if (
defaultTtl !== defaultTtl !==
props.defaultValues['channel_affinity_setting.default_ttl_seconds'] props.defaultValues['channel_affinity_setting.default_ttl_seconds']
) )
updates.push({ {updates.push({
key: 'channel_affinity_setting.default_ttl_seconds', key: 'channel_affinity_setting.default_ttl_seconds',
value: String(defaultTtl), value: String(defaultTtl),
}) })}
const origRules = props.defaultValues['channel_affinity_setting.rules'] const origRules = props.defaultValues['channel_affinity_setting.rules']
const origSerialized = (() => { const origSerialized = (() => {
...@@ -411,7 +411,7 @@ export function ChannelAffinitySection(props: Props) { ...@@ -411,7 +411,7 @@ export function ChannelAffinitySection(props: Props) {
checked={enabled} checked={enabled}
onCheckedChange={setEnabled} onCheckedChange={setEnabled}
label={t('Enable')} label={t('Enable')}
className='border-b-0 py-0' className='py-0'
/> />
<div className='grid gap-1.5'> <div className='grid gap-1.5'>
<Label>{t('Max Entries')}</Label> <Label>{t('Max Entries')}</Label>
......
...@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useEffect, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { Plus, Trash2 } from 'lucide-react' import { Plus, Trash2 } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
...@@ -44,6 +44,10 @@ import { SettingsSwitchField } from '../../components/settings-form-layout' ...@@ -44,6 +44,10 @@ import { SettingsSwitchField } from '../../components/settings-form-layout'
import { RULE_TEMPLATES } from './constants' import { RULE_TEMPLATES } from './constants'
import type { AffinityRule, KeySource } from './types' import type { AffinityRule, KeySource } from './types'
type KeySourceRow = KeySource & {
rowId: string
}
const KEY_SOURCE_TYPES = [ const KEY_SOURCE_TYPES = [
'context_int', 'context_int',
'context_string', 'context_string',
...@@ -103,8 +107,13 @@ interface Props { ...@@ -103,8 +107,13 @@ interface Props {
export function RuleEditorDialog(props: Props) { export function RuleEditorDialog(props: Props) {
const { t } = useTranslation() const { t } = useTranslation()
const isEdit = !!props.rule?.name const isEdit = !!props.rule?.name
const [keySources, setKeySources] = useState<KeySource[]>([ const nextKeySourceRowId = useRef(0)
{ type: 'gjson', path: '' }, const createKeySourceRow = (source?: Partial<KeySource>): KeySourceRow => ({
...normalizeKeySource(source ?? { type: 'gjson', path: '' }),
rowId: String(nextKeySourceRowId.current++),
})
const [keySources, setKeySources] = useState<KeySourceRow[]>(() => [
createKeySourceRow(),
]) ])
const [advancedOpen, setAdvancedOpen] = useState(false) const [advancedOpen, setAdvancedOpen] = useState(false)
...@@ -141,7 +150,11 @@ export function RuleEditorDialog(props: Props) { ...@@ -141,7 +150,11 @@ export function RuleEditorDialog(props: Props) {
: '', : '',
}) })
const sources = (r.key_sources || []).map(normalizeKeySource) const sources = (r.key_sources || []).map(normalizeKeySource)
setKeySources(sources.length > 0 ? sources : [{ type: 'gjson', path: '' }]) setKeySources(
sources.length > 0
? sources.map(createKeySourceRow)
: [createKeySourceRow()]
)
if (r.param_override_template) setAdvancedOpen(true) if (r.param_override_template) setAdvancedOpen(true)
} }
...@@ -166,7 +179,7 @@ export function RuleEditorDialog(props: Props) { ...@@ -166,7 +179,7 @@ export function RuleEditorDialog(props: Props) {
include_rule_name: true, include_rule_name: true,
param_override_template_json: '', param_override_template_json: '',
}) })
setKeySources([{ type: 'gjson', path: '' }]) setKeySources([createKeySourceRow()])
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.open, props.rule, props.templateKey]) }, [props.open, props.rule, props.templateKey])
...@@ -297,7 +310,7 @@ export function RuleEditorDialog(props: Props) { ...@@ -297,7 +310,7 @@ export function RuleEditorDialog(props: Props) {
variant='outline' variant='outline'
size='sm' size='sm'
onClick={() => onClick={() =>
setKeySources((prev) => [...prev, { type: 'gjson', path: '' }]) setKeySources((prev) => [...prev, createKeySourceRow()])
} }
> >
<Plus className='mr-1 h-3 w-3' /> <Plus className='mr-1 h-3 w-3' />
...@@ -310,21 +323,22 @@ export function RuleEditorDialog(props: Props) { ...@@ -310,21 +323,22 @@ export function RuleEditorDialog(props: Props) {
<div className='space-y-2'> <div className='space-y-2'>
{keySources.map((src, idx) => ( {keySources.map((src, idx) => (
<div <div
key={idx} key={src.rowId}
className='flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center' className='flex min-w-0 flex-col gap-2 sm:flex-row sm:items-center'
> >
<Select <Select
items={[ items={KEY_SOURCE_TYPES.map((t) => ({ value: t, label: t }))}
...KEY_SOURCE_TYPES.map((t) => ({ value: t, label: t })),
]}
value={src.type} value={src.type}
onValueChange={(v) => { onValueChange={(v) => {
if (v === null) return if (v === null) return
const next = [...keySources] const next = [...keySources]
next[idx] = normalizeKeySource({ next[idx] = {
...normalizeKeySource({
...src, ...src,
type: v as KeySource['type'], type: v as KeySource['type'],
}) }),
rowId: src.rowId,
}
setKeySources(next) setKeySources(next)
}} }}
> >
...@@ -432,19 +446,19 @@ export function RuleEditorDialog(props: Props) { ...@@ -432,19 +446,19 @@ export function RuleEditorDialog(props: Props) {
checked={form.watch('include_using_group')} checked={form.watch('include_using_group')}
onCheckedChange={(v) => form.setValue('include_using_group', v)} onCheckedChange={(v) => form.setValue('include_using_group', v)}
label={t('Include Group')} label={t('Include Group')}
className='border-b-0 py-0' className='py-0'
/> />
<SettingsSwitchField <SettingsSwitchField
checked={form.watch('include_model_name')} checked={form.watch('include_model_name')}
onCheckedChange={(v) => form.setValue('include_model_name', v)} onCheckedChange={(v) => form.setValue('include_model_name', v)}
label={t('Include Model')} label={t('Include Model')}
className='border-b-0 py-0' className='py-0'
/> />
<SettingsSwitchField <SettingsSwitchField
checked={form.watch('include_rule_name')} checked={form.watch('include_rule_name')}
onCheckedChange={(v) => form.setValue('include_rule_name', v)} onCheckedChange={(v) => form.setValue('include_rule_name', v)}
label={t('Include Rule Name')} label={t('Include Rule Name')}
className='border-b-0 py-0' className='py-0'
/> />
</div> </div>
</CollapsibleContent> </CollapsibleContent>
......
...@@ -25,11 +25,8 @@ import { ...@@ -25,11 +25,8 @@ import {
FormControl, FormControl,
FormDescription, FormDescription,
FormField, FormField,
FormItem,
FormLabel, FormLabel,
FormMessage,
} from '@/components/ui/form' } from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { import {
SettingsForm, SettingsForm,
...@@ -40,10 +37,8 @@ import { SettingsPageFormActions } from '../components/settings-page-context' ...@@ -40,10 +37,8 @@ import { SettingsPageFormActions } from '../components/settings-page-context'
import { SettingsSection } from '../components/settings-section' import { SettingsSection } from '../components/settings-section'
import { useResetForm } from '../hooks/use-reset-form' import { useResetForm } from '../hooks/use-reset-form'
import { useUpdateOption } from '../hooks/use-update-option' import { useUpdateOption } from '../hooks/use-update-option'
import { safeNumberFieldProps } from '../utils/numeric-field'
const behaviorSchema = z.object({ const behaviorSchema = z.object({
RetryTimes: z.coerce.number().min(0).max(10),
DefaultCollapseSidebar: z.boolean(), DefaultCollapseSidebar: z.boolean(),
DemoSiteEnabled: z.boolean(), DemoSiteEnabled: z.boolean(),
SelfUseModeEnabled: z.boolean(), SelfUseModeEnabled: z.boolean(),
...@@ -88,28 +83,6 @@ export function SystemBehaviorSection({ ...@@ -88,28 +83,6 @@ export function SystemBehaviorSection({
/> />
<FormField <FormField
control={form.control} control={form.control}
name='RetryTimes'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Retry Times')}</FormLabel>
<FormControl>
<Input
type='number'
min='0'
max='10'
{...safeNumberFieldProps(field)}
/>
</FormControl>
<FormDescription>
{t('Number of times to retry failed requests (0-10)')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='DefaultCollapseSidebar' name='DefaultCollapseSidebar'
render={({ field }) => ( render={({ field }) => (
<SettingsSwitchItem> <SettingsSwitchItem>
......
...@@ -98,7 +98,7 @@ export function WaffoSettingsSection({ ...@@ -98,7 +98,7 @@ export function WaffoSettingsSection({
const saveMethod = () => { const saveMethod = () => {
if (!methodForm.name.trim()) if (!methodForm.name.trim())
return toast.error(t('Payment method name is required')) {return toast.error(t('Payment method name is required'))}
if (editingIdx === -1) { if (editingIdx === -1) {
onPayMethodsChange((prev) => [...prev, methodForm]) onPayMethodsChange((prev) => [...prev, methodForm])
} else { } else {
...@@ -125,15 +125,13 @@ export function WaffoSettingsSection({ ...@@ -125,15 +125,13 @@ export function WaffoSettingsSection({
} }
const reader = new FileReader() const reader = new FileReader()
reader.onload = (loadEvent) => { reader.addEventListener('load', () => {
setMethodForm((previous) => ({ setMethodForm((previous) => ({
...previous, ...previous,
icon: icon:
typeof loadEvent.target?.result === 'string' typeof reader.result === 'string' ? reader.result : '',
? loadEvent.target.result
: '',
})) }))
} })
reader.readAsDataURL(file) reader.readAsDataURL(file)
event.target.value = '' event.target.value = ''
} }
...@@ -164,13 +162,13 @@ export function WaffoSettingsSection({ ...@@ -164,13 +162,13 @@ export function WaffoSettingsSection({
checked={values.WaffoEnabled} checked={values.WaffoEnabled}
onCheckedChange={(v) => onValueChange('WaffoEnabled', v)} onCheckedChange={(v) => onValueChange('WaffoEnabled', v)}
label={t('Enable Waffo')} label={t('Enable Waffo')}
className='border-b-0 py-0' className='py-0'
/> />
<SettingsSwitchField <SettingsSwitchField
checked={values.WaffoSandbox} checked={values.WaffoSandbox}
onCheckedChange={(v) => onValueChange('WaffoSandbox', v)} onCheckedChange={(v) => onValueChange('WaffoSandbox', v)}
label={t('Sandbox mode')} label={t('Sandbox mode')}
className='border-b-0 py-0' className='py-0'
/> />
</div> </div>
......
...@@ -270,7 +270,7 @@ export function HeaderNavigationSection({ ...@@ -270,7 +270,7 @@ export function HeaderNavigationSection({
name={module.requireAuthKey} name={module.requireAuthKey}
render={({ field }) => ( render={({ field }) => (
<SettingsControlChildren> <SettingsControlChildren>
<SettingsSwitchItem className='border-b-0 py-2'> <SettingsSwitchItem className='py-2'>
<SettingsSwitchContent> <SettingsSwitchContent>
<FormLabel>{module.requireAuthTitle}</FormLabel> <FormLabel>{module.requireAuthTitle}</FormLabel>
<FormDescription> <FormDescription>
......
...@@ -51,7 +51,7 @@ type SidebarModulesSectionProps = { ...@@ -51,7 +51,7 @@ type SidebarModulesSectionProps = {
type SidebarFormValues = SidebarModulesAdminConfig type SidebarFormValues = SidebarModulesAdminConfig
const toTitleCase = (value: string) => const toTitleCase = (value: string) =>
value.replace(/[_-]+/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()) value.replaceAll(/[_-]+/g, ' ').replaceAll(/\b\w/g, (char) => char.toUpperCase())
export function SidebarModulesSection({ export function SidebarModulesSection({
config, config,
...@@ -237,7 +237,7 @@ export function SidebarModulesSection({ ...@@ -237,7 +237,7 @@ export function SidebarModulesSection({
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
name={`${sectionKey}.${moduleKey}` as any} name={`${sectionKey}.${moduleKey}` as any}
render={({ field }) => ( render={({ field }) => (
<SettingsSwitchItem className='border-b-0 py-2'> <SettingsSwitchItem className='py-2'>
<SettingsSwitchContent> <SettingsSwitchContent>
<FormLabel>{moduleInfo.title}</FormLabel> <FormLabel>{moduleInfo.title}</FormLabel>
<FormDescription> <FormDescription>
......
...@@ -62,6 +62,16 @@ const defaultModelSettings: ModelSettings = { ...@@ -62,6 +62,16 @@ const defaultModelSettings: ModelSettings = {
AutoGroups: '', AutoGroups: '',
DefaultUseAutoGroup: false, DefaultUseAutoGroup: false,
'group_ratio_setting.group_special_usable_group': '{}', 'group_ratio_setting.group_special_usable_group': '{}',
RetryTimes: 0,
ChannelDisableThreshold: '',
AutomaticDisableChannelEnabled: false,
AutomaticEnableChannelEnabled: false,
AutomaticDisableKeywords: '',
AutomaticDisableStatusCodes: '401',
AutomaticRetryStatusCodes:
'100-199,300-399,401-407,409-499,500-503,505-523,525-599',
'monitor_setting.auto_test_channel_enabled': false,
'monitor_setting.auto_test_channel_minutes': 10,
'channel_affinity_setting.enabled': false, 'channel_affinity_setting.enabled': false,
'channel_affinity_setting.switch_on_success': true, 'channel_affinity_setting.switch_on_success': true,
'channel_affinity_setting.keep_on_channel_disabled': false, 'channel_affinity_setting.keep_on_channel_disabled': false,
......
...@@ -24,6 +24,7 @@ import { ClaudeSettingsCard } from './claude-settings-card' ...@@ -24,6 +24,7 @@ import { ClaudeSettingsCard } from './claude-settings-card'
import { GeminiSettingsCard } from './gemini-settings-card' import { GeminiSettingsCard } from './gemini-settings-card'
import { GlobalSettingsCard } from './global-settings-card' import { GlobalSettingsCard } from './global-settings-card'
import { GrokSettingsCard } from './grok-settings-card' import { GrokSettingsCard } from './grok-settings-card'
import { RoutingReliabilitySection } from './routing-reliability-section'
function formatJsonForEditor(value: string, fallback: string) { function formatJsonForEditor(value: string, fallback: string) {
const raw = (value ?? '').toString().trim() const raw = (value ?? '').toString().trim()
...@@ -65,6 +66,28 @@ const MODELS_SECTIONS = [ ...@@ -65,6 +66,28 @@ const MODELS_SECTIONS = [
), ),
}, },
{ {
id: 'routing-reliability',
titleKey: 'Routing Reliability',
build: (settings: ModelSettings) => (
<RoutingReliabilitySection
defaultValues={{
RetryTimes: settings.RetryTimes,
ChannelDisableThreshold: settings.ChannelDisableThreshold,
AutomaticDisableChannelEnabled:
settings.AutomaticDisableChannelEnabled,
AutomaticEnableChannelEnabled: settings.AutomaticEnableChannelEnabled,
AutomaticDisableKeywords: settings.AutomaticDisableKeywords,
AutomaticDisableStatusCodes: settings.AutomaticDisableStatusCodes,
AutomaticRetryStatusCodes: settings.AutomaticRetryStatusCodes,
'monitor_setting.auto_test_channel_enabled':
settings['monitor_setting.auto_test_channel_enabled'],
'monitor_setting.auto_test_channel_minutes':
settings['monitor_setting.auto_test_channel_minutes'],
}}
/>
),
},
{
id: 'gemini', id: 'gemini',
titleKey: 'Gemini', titleKey: 'Gemini',
build: (settings: ModelSettings) => ( build: (settings: ModelSettings) => (
......
...@@ -26,20 +26,10 @@ import { ...@@ -26,20 +26,10 @@ import {
} from './section-registry.tsx' } from './section-registry.tsx'
const defaultOperationsSettings: OperationsSettings = { const defaultOperationsSettings: OperationsSettings = {
RetryTimes: 0,
DefaultCollapseSidebar: false, DefaultCollapseSidebar: false,
DemoSiteEnabled: false, DemoSiteEnabled: false,
SelfUseModeEnabled: false, SelfUseModeEnabled: false,
ChannelDisableThreshold: '',
QuotaRemindThreshold: '', QuotaRemindThreshold: '',
AutomaticDisableChannelEnabled: false,
AutomaticEnableChannelEnabled: false,
AutomaticDisableKeywords: '',
AutomaticDisableStatusCodes: '401',
AutomaticRetryStatusCodes:
'100-199,300-399,401-407,409-499,500-503,505-523,525-599',
'monitor_setting.auto_test_channel_enabled': false,
'monitor_setting.auto_test_channel_minutes': 10,
SMTPServer: '', SMTPServer: '',
SMTPPort: '', SMTPPort: '',
SMTPAccount: '', SMTPAccount: '',
......
...@@ -33,7 +33,6 @@ const OPERATIONS_SECTIONS = [ ...@@ -33,7 +33,6 @@ const OPERATIONS_SECTIONS = [
build: (settings: OperationsSettings) => ( build: (settings: OperationsSettings) => (
<SystemBehaviorSection <SystemBehaviorSection
defaultValues={{ defaultValues={{
RetryTimes: settings.RetryTimes,
DefaultCollapseSidebar: settings.DefaultCollapseSidebar, DefaultCollapseSidebar: settings.DefaultCollapseSidebar,
DemoSiteEnabled: settings.DemoSiteEnabled, DemoSiteEnabled: settings.DemoSiteEnabled,
SelfUseModeEnabled: settings.SelfUseModeEnabled, SelfUseModeEnabled: settings.SelfUseModeEnabled,
...@@ -42,23 +41,20 @@ const OPERATIONS_SECTIONS = [ ...@@ -42,23 +41,20 @@ const OPERATIONS_SECTIONS = [
), ),
}, },
{ {
id: 'monitoring', id: 'alerts',
titleKey: 'Monitoring & Alerts', titleKey: 'Monitoring & Alerts',
build: (settings: OperationsSettings) => ( build: (settings: OperationsSettings) => (
<MonitoringSettingsSection <MonitoringSettingsSection
defaultValues={{ defaultValues={{
ChannelDisableThreshold: settings.ChannelDisableThreshold,
QuotaRemindThreshold: settings.QuotaRemindThreshold, QuotaRemindThreshold: settings.QuotaRemindThreshold,
AutomaticDisableChannelEnabled: 'perf_metrics_setting.enabled':
settings.AutomaticDisableChannelEnabled, settings['perf_metrics_setting.enabled'] ?? true,
AutomaticEnableChannelEnabled: settings.AutomaticEnableChannelEnabled, 'perf_metrics_setting.flush_interval':
AutomaticDisableKeywords: settings.AutomaticDisableKeywords, settings['perf_metrics_setting.flush_interval'] ?? 5,
AutomaticDisableStatusCodes: settings.AutomaticDisableStatusCodes, 'perf_metrics_setting.bucket_time':
AutomaticRetryStatusCodes: settings.AutomaticRetryStatusCodes, settings['perf_metrics_setting.bucket_time'] ?? 'hour',
'monitor_setting.auto_test_channel_enabled': 'perf_metrics_setting.retention_days':
settings['monitor_setting.auto_test_channel_enabled'], settings['perf_metrics_setting.retention_days'] ?? 0,
'monitor_setting.auto_test_channel_minutes':
settings['monitor_setting.auto_test_channel_minutes'],
}} }}
/> />
), ),
...@@ -125,14 +121,6 @@ const OPERATIONS_SECTIONS = [ ...@@ -125,14 +121,6 @@ const OPERATIONS_SECTIONS = [
settings['performance_setting.monitor_memory_threshold'] ?? 90, settings['performance_setting.monitor_memory_threshold'] ?? 90,
'performance_setting.monitor_disk_threshold': 'performance_setting.monitor_disk_threshold':
settings['performance_setting.monitor_disk_threshold'] ?? 95, settings['performance_setting.monitor_disk_threshold'] ?? 95,
'perf_metrics_setting.enabled':
settings['perf_metrics_setting.enabled'] ?? true,
'perf_metrics_setting.flush_interval':
settings['perf_metrics_setting.flush_interval'] ?? 5,
'perf_metrics_setting.bucket_time':
settings['perf_metrics_setting.bucket_time'] ?? 'hour',
'perf_metrics_setting.retention_days':
settings['perf_metrics_setting.retention_days'] ?? 0,
}} }}
/> />
), ),
......
...@@ -174,6 +174,15 @@ export type ModelSettings = { ...@@ -174,6 +174,15 @@ export type ModelSettings = {
AutoGroups: string AutoGroups: string
DefaultUseAutoGroup: boolean DefaultUseAutoGroup: boolean
'group_ratio_setting.group_special_usable_group': string 'group_ratio_setting.group_special_usable_group': string
RetryTimes: number
ChannelDisableThreshold: string
AutomaticDisableChannelEnabled: boolean
AutomaticEnableChannelEnabled: boolean
AutomaticDisableKeywords: string
AutomaticDisableStatusCodes: string
AutomaticRetryStatusCodes: string
'monitor_setting.auto_test_channel_enabled': boolean
'monitor_setting.auto_test_channel_minutes': number
'channel_affinity_setting.enabled': boolean 'channel_affinity_setting.enabled': boolean
'channel_affinity_setting.switch_on_success': boolean 'channel_affinity_setting.switch_on_success': boolean
'channel_affinity_setting.keep_on_channel_disabled': boolean 'channel_affinity_setting.keep_on_channel_disabled': boolean
...@@ -270,19 +279,10 @@ export type BillingSettings = { ...@@ -270,19 +279,10 @@ export type BillingSettings = {
} }
export type OperationsSettings = { export type OperationsSettings = {
RetryTimes: number
DefaultCollapseSidebar: boolean DefaultCollapseSidebar: boolean
DemoSiteEnabled: boolean DemoSiteEnabled: boolean
SelfUseModeEnabled: boolean SelfUseModeEnabled: boolean
ChannelDisableThreshold: string
QuotaRemindThreshold: string QuotaRemindThreshold: string
AutomaticDisableChannelEnabled: boolean
AutomaticEnableChannelEnabled: boolean
AutomaticDisableKeywords: string
AutomaticDisableStatusCodes: string
AutomaticRetryStatusCodes: string
'monitor_setting.auto_test_channel_enabled': boolean
'monitor_setting.auto_test_channel_minutes': number
SMTPServer: string SMTPServer: string
SMTPPort: string SMTPPort: string
SMTPAccount: string SMTPAccount: string
......
...@@ -473,6 +473,7 @@ ...@@ -473,6 +473,7 @@
"Auto Group Chain": "Auto Group Chain", "Auto Group Chain": "Auto Group Chain",
"Auto refresh": "Auto refresh", "Auto refresh": "Auto refresh",
"Auto Sync Upstream Models": "Auto Sync Upstream Models", "Auto Sync Upstream Models": "Auto Sync Upstream Models",
"Auto-disable rules": "Auto-disable rules",
"Auto-disable status codes": "Auto-disable status codes", "Auto-disable status codes": "Auto-disable status codes",
"Auto-discover": "Auto-discover", "Auto-discover": "Auto-discover",
"Auto-discovers endpoints from the provider": "Auto-discovers endpoints from the provider", "Auto-discovers endpoints from the provider": "Auto-discovers endpoints from the provider",
...@@ -683,6 +684,7 @@ ...@@ -683,6 +684,7 @@
"Channel Affinity": "Channel Affinity", "Channel Affinity": "Channel Affinity",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.", "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.",
"Channel Affinity: Upstream Cache Hit": "Channel Affinity: Upstream Cache Hit", "Channel Affinity: Upstream Cache Hit": "Channel Affinity: Upstream Cache Hit",
"Channel health checks": "Channel health checks",
"Channel copied successfully": "Channel copied successfully", "Channel copied successfully": "Channel copied successfully",
"Channel created successfully": "Channel created successfully", "Channel created successfully": "Channel created successfully",
"Channel deleted successfully": "Channel deleted successfully", "Channel deleted successfully": "Channel deleted successfully",
...@@ -2391,6 +2393,7 @@ ...@@ -2391,6 +2393,7 @@
"Max Disk Cache Size (MB)": "Max Disk Cache Size (MB)", "Max Disk Cache Size (MB)": "Max Disk Cache Size (MB)",
"Max Entries": "Max Entries", "Max Entries": "Max Entries",
"Max output": "Max output", "Max output": "Max output",
"Max Retries": "Max Retries",
"Max Requests (incl. failures)": "Max Requests (incl. failures)", "Max Requests (incl. failures)": "Max Requests (incl. failures)",
"Max Requests (including failures)": "Max Requests (including failures)", "Max Requests (including failures)": "Max Requests (including failures)",
"Max requests per period": "Max requests per period", "Max requests per period": "Max requests per period",
...@@ -3541,11 +3544,13 @@ ...@@ -3541,11 +3544,13 @@
"Restore defaults": "Restore defaults", "Restore defaults": "Restore defaults",
"Restrict user model request frequency (may impact high concurrency performance)": "Restrict user model request frequency (may impact high concurrency performance)", "Restrict user model request frequency (may impact high concurrency performance)": "Restrict user model request frequency (may impact high concurrency performance)",
"Result": "Result", "Result": "Result",
"Request retry": "Request retry",
"Retain last N days": "Retain last N days", "Retain last N days": "Retain last N days",
"Retain last N files": "Retain last N files", "Retain last N files": "Retain last N files",
"Retention days": "Retention days", "Retention days": "Retention days",
"Retry": "Retry", "Retry": "Retry",
"Retry Chain": "Retry Chain", "Retry Chain": "Retry Chain",
"Retry Settings": "Retry Settings",
"Retry Suggestion": "Retry Suggestion", "Retry Suggestion": "Retry Suggestion",
"Retry Times": "Retry Times", "Retry Times": "Retry Times",
"Return a custom error immediately": "Return a custom error immediately", "Return a custom error immediately": "Return a custom error immediately",
...@@ -3576,6 +3581,7 @@ ...@@ -3576,6 +3581,7 @@
"Route, auth, and balance check in one place": "Route, auth, and balance check in one place", "Route, auth, and balance check in one place": "Route, auth, and balance check in one place",
"Routes": "Routes", "Routes": "Routes",
"Routing & Overrides": "Routing & Overrides", "Routing & Overrides": "Routing & Overrides",
"Routing Reliability": "Routing Reliability",
"Routing Strategy": "Routing Strategy", "Routing Strategy": "Routing Strategy",
"Rows per page": "Rows per page", "Rows per page": "Rows per page",
"RPM": "RPM", "RPM": "RPM",
......
...@@ -473,6 +473,7 @@ ...@@ -473,6 +473,7 @@
"Auto Group Chain": "Chaîne de groupes automatique", "Auto Group Chain": "Chaîne de groupes automatique",
"Auto refresh": "Actualisation automatique", "Auto refresh": "Actualisation automatique",
"Auto Sync Upstream Models": "Synchronisation automatique des modèles en amont", "Auto Sync Upstream Models": "Synchronisation automatique des modèles en amont",
"Auto-disable rules": "Règles de désactivation automatique",
"Auto-disable status codes": "Codes de statut de désactivation auto", "Auto-disable status codes": "Codes de statut de désactivation auto",
"Auto-discover": "Découverte automatique", "Auto-discover": "Découverte automatique",
"Auto-discovers endpoints from the provider": "Découvre automatiquement les points de terminaison du fournisseur", "Auto-discovers endpoints from the provider": "Découvre automatiquement les points de terminaison du fournisseur",
...@@ -683,6 +684,7 @@ ...@@ -683,6 +684,7 @@
"Channel Affinity": "Affinité de canal", "Channel Affinity": "Affinité de canal",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "L'affinité de canal réutilise le dernier canal ayant réussi, en se basant sur les clés extraites du contexte de la requête ou du corps JSON.", "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "L'affinité de canal réutilise le dernier canal ayant réussi, en se basant sur les clés extraites du contexte de la requête ou du corps JSON.",
"Channel Affinity: Upstream Cache Hit": "Affinité de canal : hit de cache en amont", "Channel Affinity: Upstream Cache Hit": "Affinité de canal : hit de cache en amont",
"Channel health checks": "Contrôles de santé des canaux",
"Channel copied successfully": "Canal copié avec succès", "Channel copied successfully": "Canal copié avec succès",
"Channel created successfully": "Canal créé avec succès", "Channel created successfully": "Canal créé avec succès",
"Channel deleted successfully": "Canal supprimé avec succès", "Channel deleted successfully": "Canal supprimé avec succès",
...@@ -2391,6 +2393,7 @@ ...@@ -2391,6 +2393,7 @@
"Max Disk Cache Size (MB)": "Taille max du cache disque (Mo)", "Max Disk Cache Size (MB)": "Taille max du cache disque (Mo)",
"Max Entries": "Entrées max", "Max Entries": "Entrées max",
"Max output": "Sortie max", "Max output": "Sortie max",
"Max Retries": "Relances max",
"Max Requests (incl. failures)": "Max Requêtes (incl. échecs)", "Max Requests (incl. failures)": "Max Requêtes (incl. échecs)",
"Max Requests (including failures)": "Max Requêtes (incluant les échecs)", "Max Requests (including failures)": "Max Requêtes (incluant les échecs)",
"Max requests per period": "Nombre max de requêtes par période", "Max requests per period": "Nombre max de requêtes par période",
...@@ -3541,11 +3544,13 @@ ...@@ -3541,11 +3544,13 @@
"Restore defaults": "Restaurer les paramètres par défaut", "Restore defaults": "Restaurer les paramètres par défaut",
"Restrict user model request frequency (may impact high concurrency performance)": "Restreindre la fréquence des requêtes du modèle utilisateur (peut impacter les performances en cas de forte concurrence)", "Restrict user model request frequency (may impact high concurrency performance)": "Restreindre la fréquence des requêtes du modèle utilisateur (peut impacter les performances en cas de forte concurrence)",
"Result": "Résultat", "Result": "Résultat",
"Request retry": "Relance des requêtes",
"Retain last N days": "Conserver les N derniers jours", "Retain last N days": "Conserver les N derniers jours",
"Retain last N files": "Conserver les N derniers fichiers", "Retain last N files": "Conserver les N derniers fichiers",
"Retention days": "Jours de rétention", "Retention days": "Jours de rétention",
"Retry": "Réessayer", "Retry": "Réessayer",
"Retry Chain": "Chaîne de tentatives", "Retry Chain": "Chaîne de tentatives",
"Retry Settings": "Paramètres de relance",
"Retry Suggestion": "Suggestion de relance", "Retry Suggestion": "Suggestion de relance",
"Retry Times": "Nombre de tentatives", "Retry Times": "Nombre de tentatives",
"Return a custom error immediately": "Retourner immédiatement une erreur personnalisée", "Return a custom error immediately": "Retourner immédiatement une erreur personnalisée",
...@@ -3576,6 +3581,7 @@ ...@@ -3576,6 +3581,7 @@
"Route, auth, and balance check in one place": "Routage, authentification et solde au même endroit", "Route, auth, and balance check in one place": "Routage, authentification et solde au même endroit",
"Routes": "Routes", "Routes": "Routes",
"Routing & Overrides": "Routage et surcharges", "Routing & Overrides": "Routage et surcharges",
"Routing Reliability": "Fiabilité du routage",
"Routing Strategy": "Stratégie de routage", "Routing Strategy": "Stratégie de routage",
"Rows per page": "Lignes par page", "Rows per page": "Lignes par page",
"RPM": "RPM", "RPM": "RPM",
......
...@@ -473,6 +473,7 @@ ...@@ -473,6 +473,7 @@
"Auto Group Chain": "自動グループチェーン", "Auto Group Chain": "自動グループチェーン",
"Auto refresh": "自動更新", "Auto refresh": "自動更新",
"Auto Sync Upstream Models": "アップストリームモデルの自動同期", "Auto Sync Upstream Models": "アップストリームモデルの自動同期",
"Auto-disable rules": "自動無効化ルール",
"Auto-disable status codes": "自動無効化するステータスコード", "Auto-disable status codes": "自動無効化するステータスコード",
"Auto-discover": "自動検出", "Auto-discover": "自動検出",
"Auto-discovers endpoints from the provider": "プロバイダーからエンドポイントを自動検出します", "Auto-discovers endpoints from the provider": "プロバイダーからエンドポイントを自動検出します",
...@@ -683,6 +684,7 @@ ...@@ -683,6 +684,7 @@
"Channel Affinity": "チャネルアフィニティ", "Channel Affinity": "チャネルアフィニティ",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "チャネルアフィニティは、リクエストコンテキストまたは JSON Body から抽出したキーに基づいて、前回成功したチャネルを優先的に再利用します。", "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "チャネルアフィニティは、リクエストコンテキストまたは JSON Body から抽出したキーに基づいて、前回成功したチャネルを優先的に再利用します。",
"Channel Affinity: Upstream Cache Hit": "チャネルアフィニティ:上流キャッシュヒット", "Channel Affinity: Upstream Cache Hit": "チャネルアフィニティ:上流キャッシュヒット",
"Channel health checks": "チャネルヘルスチェック",
"Channel copied successfully": "チャネルが正常にコピーされました", "Channel copied successfully": "チャネルが正常にコピーされました",
"Channel created successfully": "チャネルが正常に作成されました", "Channel created successfully": "チャネルが正常に作成されました",
"Channel deleted successfully": "チャネルが正常に削除されました", "Channel deleted successfully": "チャネルが正常に削除されました",
...@@ -2391,6 +2393,7 @@ ...@@ -2391,6 +2393,7 @@
"Max Disk Cache Size (MB)": "ディスクキャッシュ最大容量 (MB)", "Max Disk Cache Size (MB)": "ディスクキャッシュ最大容量 (MB)",
"Max Entries": "最大エントリ数", "Max Entries": "最大エントリ数",
"Max output": "最大出力", "Max output": "最大出力",
"Max Retries": "最大再試行",
"Max Requests (incl. failures)": "最大リクエスト数(失敗を含む)", "Max Requests (incl. failures)": "最大リクエスト数(失敗を含む)",
"Max Requests (including failures)": "最大リクエスト数(失敗を含む)", "Max Requests (including failures)": "最大リクエスト数(失敗を含む)",
"Max requests per period": "期間あたりの最大リクエスト数", "Max requests per period": "期間あたりの最大リクエスト数",
...@@ -3541,11 +3544,13 @@ ...@@ -3541,11 +3544,13 @@
"Restore defaults": "既定に戻す", "Restore defaults": "既定に戻す",
"Restrict user model request frequency (may impact high concurrency performance)": "ユーザーモデルのリクエスト頻度を制限する(高並行性パフォーマンスに影響を与える可能性があります)", "Restrict user model request frequency (may impact high concurrency performance)": "ユーザーモデルのリクエスト頻度を制限する(高並行性パフォーマンスに影響を与える可能性があります)",
"Result": "結果", "Result": "結果",
"Request retry": "リクエスト再試行",
"Retain last N days": "最新N日間を保持", "Retain last N days": "最新N日間を保持",
"Retain last N files": "最新N個のファイルを保持", "Retain last N files": "最新N個のファイルを保持",
"Retention days": "保持日数", "Retention days": "保持日数",
"Retry": "再試行", "Retry": "再試行",
"Retry Chain": "リトライチェーン", "Retry Chain": "リトライチェーン",
"Retry Settings": "再試行設定",
"Retry Suggestion": "リトライ提案", "Retry Suggestion": "リトライ提案",
"Retry Times": "再試行回数", "Retry Times": "再試行回数",
"Return a custom error immediately": "即座にカスタムエラーを返す", "Return a custom error immediately": "即座にカスタムエラーを返す",
...@@ -3576,6 +3581,7 @@ ...@@ -3576,6 +3581,7 @@
"Route, auth, and balance check in one place": "ルート、認証、残高確認を一か所に集約", "Route, auth, and balance check in one place": "ルート、認証、残高確認を一か所に集約",
"Routes": "ルート", "Routes": "ルート",
"Routing & Overrides": "ルーティングと上書き", "Routing & Overrides": "ルーティングと上書き",
"Routing Reliability": "ルーティング信頼性",
"Routing Strategy": "ルーティング戦略", "Routing Strategy": "ルーティング戦略",
"Rows per page": "ページあたりの行数", "Rows per page": "ページあたりの行数",
"RPM": "RPM", "RPM": "RPM",
......
...@@ -473,6 +473,7 @@ ...@@ -473,6 +473,7 @@
"Auto Group Chain": "Автоматическая цепочка групп", "Auto Group Chain": "Автоматическая цепочка групп",
"Auto refresh": "Автообновление", "Auto refresh": "Автообновление",
"Auto Sync Upstream Models": "Автоматическая синхронизация моделей провайдера", "Auto Sync Upstream Models": "Автоматическая синхронизация моделей провайдера",
"Auto-disable rules": "Правила автоотключения",
"Auto-disable status codes": "Коды автоотключения", "Auto-disable status codes": "Коды автоотключения",
"Auto-discover": "Автообнаружение", "Auto-discover": "Автообнаружение",
"Auto-discovers endpoints from the provider": "Автоматически обнаруживает конечные точки от провайдера", "Auto-discovers endpoints from the provider": "Автоматически обнаруживает конечные точки от провайдера",
...@@ -683,6 +684,7 @@ ...@@ -683,6 +684,7 @@
"Channel Affinity": "Привязка к каналу", "Channel Affinity": "Привязка к каналу",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Привязка к каналу повторно использует последний успешный канал на основе ключей, извлечённых из контекста запроса или тела JSON.", "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Привязка к каналу повторно использует последний успешный канал на основе ключей, извлечённых из контекста запроса или тела JSON.",
"Channel Affinity: Upstream Cache Hit": "Привязка к каналу: попадание в кэш upstream", "Channel Affinity: Upstream Cache Hit": "Привязка к каналу: попадание в кэш upstream",
"Channel health checks": "Проверки состояния каналов",
"Channel copied successfully": "Канал успешно скопирован", "Channel copied successfully": "Канал успешно скопирован",
"Channel created successfully": "Канал успешно создан", "Channel created successfully": "Канал успешно создан",
"Channel deleted successfully": "Канал успешно удалён", "Channel deleted successfully": "Канал успешно удалён",
...@@ -2391,6 +2393,7 @@ ...@@ -2391,6 +2393,7 @@
"Max Disk Cache Size (MB)": "Макс. размер дискового кэша (МБ)", "Max Disk Cache Size (MB)": "Макс. размер дискового кэша (МБ)",
"Max Entries": "Макс. записей", "Max Entries": "Макс. записей",
"Max output": "Макс. вывод", "Max output": "Макс. вывод",
"Max Retries": "Макс. повторов",
"Max Requests (incl. failures)": "Макс. запросов (вкл. сбои)", "Max Requests (incl. failures)": "Макс. запросов (вкл. сбои)",
"Max Requests (including failures)": "Макс. запросов (включая сбои)", "Max Requests (including failures)": "Макс. запросов (включая сбои)",
"Max requests per period": "Макс. запросов за период", "Max requests per period": "Макс. запросов за период",
...@@ -3541,11 +3544,13 @@ ...@@ -3541,11 +3544,13 @@
"Restore defaults": "Сбросить к значениям по умолчанию", "Restore defaults": "Сбросить к значениям по умолчанию",
"Restrict user model request frequency (may impact high concurrency performance)": "Ограничить частоту запросов пользовательских моделей (может повлиять на производительность при высокой конкуренции)", "Restrict user model request frequency (may impact high concurrency performance)": "Ограничить частоту запросов пользовательских моделей (может повлиять на производительность при высокой конкуренции)",
"Result": "Результат", "Result": "Результат",
"Request retry": "Повтор запросов",
"Retain last N days": "Хранить последние N дней", "Retain last N days": "Хранить последние N дней",
"Retain last N files": "Хранить последние N файлов", "Retain last N files": "Хранить последние N файлов",
"Retention days": "Дней хранения", "Retention days": "Дней хранения",
"Retry": "Повторить попытку", "Retry": "Повторить попытку",
"Retry Chain": "Цепочка повторов", "Retry Chain": "Цепочка повторов",
"Retry Settings": "Настройки повторов",
"Retry Suggestion": "Рекомендация по повтору", "Retry Suggestion": "Рекомендация по повтору",
"Retry Times": "Количество повторных попыток", "Retry Times": "Количество повторных попыток",
"Return a custom error immediately": "Немедленно вернуть пользовательскую ошибку", "Return a custom error immediately": "Немедленно вернуть пользовательскую ошибку",
...@@ -3576,6 +3581,7 @@ ...@@ -3576,6 +3581,7 @@
"Route, auth, and balance check in one place": "Маршрут, аутентификация и баланс в одном месте", "Route, auth, and balance check in one place": "Маршрут, аутентификация и баланс в одном месте",
"Routes": "Маршруты", "Routes": "Маршруты",
"Routing & Overrides": "Маршрутизация и переопределения", "Routing & Overrides": "Маршрутизация и переопределения",
"Routing Reliability": "Надежность маршрутизации",
"Routing Strategy": "Стратегия маршрутизации", "Routing Strategy": "Стратегия маршрутизации",
"Rows per page": "Строк на страницу", "Rows per page": "Строк на страницу",
"RPM": "RPM", "RPM": "RPM",
......
...@@ -473,6 +473,7 @@ ...@@ -473,6 +473,7 @@
"Auto Group Chain": "Chuỗi nhóm tự động", "Auto Group Chain": "Chuỗi nhóm tự động",
"Auto refresh": "Tự động làm mới", "Auto refresh": "Tự động làm mới",
"Auto Sync Upstream Models": "Tự động đồng bộ mô hình nguồn", "Auto Sync Upstream Models": "Tự động đồng bộ mô hình nguồn",
"Auto-disable rules": "Quy tắc tự động tắt",
"Auto-disable status codes": "Mã trạng thái tự tắt", "Auto-disable status codes": "Mã trạng thái tự tắt",
"Auto-discover": "Tự động khám phá", "Auto-discover": "Tự động khám phá",
"Auto-discovers endpoints from the provider": "Tự động khám phá các điểm cuối từ nhà cung cấp", "Auto-discovers endpoints from the provider": "Tự động khám phá các điểm cuối từ nhà cung cấp",
...@@ -683,6 +684,7 @@ ...@@ -683,6 +684,7 @@
"Channel Affinity": "Ưu tiên kênh", "Channel Affinity": "Ưu tiên kênh",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Ưu tiên kênh sẽ sử dụng lại kênh thành công gần nhất dựa trên các khóa được trích xuất từ ngữ cảnh yêu cầu hoặc JSON body.", "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "Ưu tiên kênh sẽ sử dụng lại kênh thành công gần nhất dựa trên các khóa được trích xuất từ ngữ cảnh yêu cầu hoặc JSON body.",
"Channel Affinity: Upstream Cache Hit": "Ưu tiên kênh: Cache hit từ upstream", "Channel Affinity: Upstream Cache Hit": "Ưu tiên kênh: Cache hit từ upstream",
"Channel health checks": "Kiểm tra tình trạng kênh",
"Channel copied successfully": "Sao chép kênh thành công", "Channel copied successfully": "Sao chép kênh thành công",
"Channel created successfully": "Tạo kênh thành công", "Channel created successfully": "Tạo kênh thành công",
"Channel deleted successfully": "Xóa kênh thành công", "Channel deleted successfully": "Xóa kênh thành công",
...@@ -2391,6 +2393,7 @@ ...@@ -2391,6 +2393,7 @@
"Max Disk Cache Size (MB)": "Dung lượng tối đa bộ nhớ đệm đĩa (MB)", "Max Disk Cache Size (MB)": "Dung lượng tối đa bộ nhớ đệm đĩa (MB)",
"Max Entries": "Số mục tối đa", "Max Entries": "Số mục tối đa",
"Max output": "Đầu ra tối đa", "Max output": "Đầu ra tối đa",
"Max Retries": "Số lần thử lại tối đa",
"Max Requests (incl. failures)": "Maximum number of requests (including errors)", "Max Requests (incl. failures)": "Maximum number of requests (including errors)",
"Max Requests (including failures)": "Số yêu cầu tối đa (bao gồm cả các lỗi)", "Max Requests (including failures)": "Số yêu cầu tối đa (bao gồm cả các lỗi)",
"Max requests per period": "Yêu cầu tối đa mỗi khoảng thời gian", "Max requests per period": "Yêu cầu tối đa mỗi khoảng thời gian",
...@@ -3541,11 +3544,13 @@ ...@@ -3541,11 +3544,13 @@
"Restore defaults": "Khôi phục mặc định", "Restore defaults": "Khôi phục mặc định",
"Restrict user model request frequency (may impact high concurrency performance)": "Hạn chế tần suất yêu cầu mô hình người dùng (có thể ảnh hưởng đến hiệu suất khi có độ đồng thời cao)", "Restrict user model request frequency (may impact high concurrency performance)": "Hạn chế tần suất yêu cầu mô hình người dùng (có thể ảnh hưởng đến hiệu suất khi có độ đồng thời cao)",
"Result": "Kết quả", "Result": "Kết quả",
"Request retry": "Thử lại yêu cầu",
"Retain last N days": "Giữ lại N ngày gần nhất", "Retain last N days": "Giữ lại N ngày gần nhất",
"Retain last N files": "Giữ lại N tệp gần nhất", "Retain last N files": "Giữ lại N tệp gần nhất",
"Retention days": "Số ngày lưu giữ", "Retention days": "Số ngày lưu giữ",
"Retry": "Thử lại", "Retry": "Thử lại",
"Retry Chain": "Chuỗi thử lại", "Retry Chain": "Chuỗi thử lại",
"Retry Settings": "Cài đặt thử lại",
"Retry Suggestion": "Gợi ý thử lại", "Retry Suggestion": "Gợi ý thử lại",
"Retry Times": "Số lần thử lại", "Retry Times": "Số lần thử lại",
"Return a custom error immediately": "Trả về lỗi tùy chỉnh ngay lập tức", "Return a custom error immediately": "Trả về lỗi tùy chỉnh ngay lập tức",
...@@ -3576,6 +3581,7 @@ ...@@ -3576,6 +3581,7 @@
"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", "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": "Route",
"Routing & Overrides": "Định tuyến & ghi đè", "Routing & Overrides": "Định tuyến & ghi đè",
"Routing Reliability": "Độ tin cậy định tuyến",
"Routing Strategy": "Chiến lược định tuyến", "Routing Strategy": "Chiến lược định tuyến",
"Rows per page": "Số hàng trên trang", "Rows per page": "Số hàng trên trang",
"RPM": "RPM", "RPM": "RPM",
......
...@@ -473,6 +473,7 @@ ...@@ -473,6 +473,7 @@
"Auto Group Chain": "自动分组链", "Auto Group Chain": "自动分组链",
"Auto refresh": "自动刷新", "Auto refresh": "自动刷新",
"Auto Sync Upstream Models": "自动同步上游模型", "Auto Sync Upstream Models": "自动同步上游模型",
"Auto-disable rules": "自动禁用规则",
"Auto-disable status codes": "自动禁用状态码", "Auto-disable status codes": "自动禁用状态码",
"Auto-discover": "自动发现", "Auto-discover": "自动发现",
"Auto-discovers endpoints from the provider": "自动从提供商发现端点", "Auto-discovers endpoints from the provider": "自动从提供商发现端点",
...@@ -683,6 +684,7 @@ ...@@ -683,6 +684,7 @@
"Channel Affinity": "渠道亲和性", "Channel Affinity": "渠道亲和性",
"Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。", "Channel affinity reuses the last successful channel based on keys extracted from the request context or JSON body.": "渠道亲和性会基于从请求上下文或 JSON Body 提取的 Key,优先复用上一次成功的渠道。",
"Channel Affinity: Upstream Cache Hit": "渠道亲和性:上游缓存命中", "Channel Affinity: Upstream Cache Hit": "渠道亲和性:上游缓存命中",
"Channel health checks": "渠道健康检查",
"Channel copied successfully": "渠道复制成功", "Channel copied successfully": "渠道复制成功",
"Channel created successfully": "渠道创建成功", "Channel created successfully": "渠道创建成功",
"Channel deleted successfully": "渠道删除成功", "Channel deleted successfully": "渠道删除成功",
...@@ -2391,6 +2393,7 @@ ...@@ -2391,6 +2393,7 @@
"Max Disk Cache Size (MB)": "磁盘缓存最大总量 (MB)", "Max Disk Cache Size (MB)": "磁盘缓存最大总量 (MB)",
"Max Entries": "最大条目数", "Max Entries": "最大条目数",
"Max output": "最大输出", "Max output": "最大输出",
"Max Retries": "最大重试",
"Max Requests (incl. failures)": "最大请求数(包括失败)", "Max Requests (incl. failures)": "最大请求数(包括失败)",
"Max Requests (including failures)": "最大请求数(包括失败)", "Max Requests (including failures)": "最大请求数(包括失败)",
"Max requests per period": "每周期最大请求数", "Max requests per period": "每周期最大请求数",
...@@ -3541,11 +3544,13 @@ ...@@ -3541,11 +3544,13 @@
"Restore defaults": "恢复默认", "Restore defaults": "恢复默认",
"Restrict user model request frequency (may impact high concurrency performance)": "限制用户模型请求频率(可能会影响高并发性能)", "Restrict user model request frequency (may impact high concurrency performance)": "限制用户模型请求频率(可能会影响高并发性能)",
"Result": "结果", "Result": "结果",
"Request retry": "请求重试",
"Retain last N days": "保留最近N天", "Retain last N days": "保留最近N天",
"Retain last N files": "保留最近 N 个文件", "Retain last N files": "保留最近 N 个文件",
"Retention days": "保留天数", "Retention days": "保留天数",
"Retry": "重试", "Retry": "重试",
"Retry Chain": "重试链路", "Retry Chain": "重试链路",
"Retry Settings": "重试设置",
"Retry Suggestion": "重试建议", "Retry Suggestion": "重试建议",
"Retry Times": "重试次数", "Retry Times": "重试次数",
"Return a custom error immediately": "立即返回自定义错误", "Return a custom error immediately": "立即返回自定义错误",
...@@ -3576,6 +3581,7 @@ ...@@ -3576,6 +3581,7 @@
"Route, auth, and balance check in one place": "路由、认证和余额检查集中展示", "Route, auth, and balance check in one place": "路由、认证和余额检查集中展示",
"Routes": "路由", "Routes": "路由",
"Routing & Overrides": "路由与覆盖", "Routing & Overrides": "路由与覆盖",
"Routing Reliability": "路由可靠性",
"Routing Strategy": "路由策略", "Routing Strategy": "路由策略",
"Rows per page": "每页行数", "Rows per page": "每页行数",
"RPM": "RPM", "RPM": "RPM",
......
...@@ -30,6 +30,9 @@ export const STATIC_I18N_KEYS = [ ...@@ -30,6 +30,9 @@ export const STATIC_I18N_KEYS = [
// Sidebar views (drill-in workspaces) // Sidebar views (drill-in workspaces)
'System Settings', 'System Settings',
'Back to Dashboard', 'Back to Dashboard',
'Auto-disable rules',
'Channel health checks',
'Request retry',
// System settings sidebar // System settings sidebar
'System Administration', 'System Administration',
...@@ -39,6 +42,7 @@ export const STATIC_I18N_KEYS = [ ...@@ -39,6 +42,7 @@ export const STATIC_I18N_KEYS = [
'Content', 'Content',
'Integrations', 'Integrations',
'Models', 'Models',
'Routing Reliability',
'Maintenance', 'Maintenance',
// Pricing constants // Pricing constants
......
...@@ -27,6 +27,13 @@ export const Route = createFileRoute( ...@@ -27,6 +27,13 @@ export const Route = createFileRoute(
'/_authenticated/system-settings/operations/$section' '/_authenticated/system-settings/operations/$section'
)({ )({
beforeLoad: ({ params }) => { beforeLoad: ({ params }) => {
if (params.section === 'monitoring') {
throw redirect({
to: '/system-settings/models/$section',
params: { section: 'routing-reliability' },
})
}
const validSections = OPERATIONS_SECTION_IDS as unknown as string[] const validSections = OPERATIONS_SECTION_IDS as unknown as string[]
if (!validSections.includes(params.section)) { if (!validSections.includes(params.section)) {
throw redirect({ throw redirect({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment