Commit 0f9f094a by CaIon

feat(default): reorganize system settings pricing UI

Refine the default system settings structure and model pricing editor so pricing configuration is easier to scan and edit.
parent 9acf5fec
......@@ -119,7 +119,7 @@ export function WorkspaceSwitcher({
// Only navigate, let useEffect synchronize workspace state based on new pathname
// This avoids race conditions and context loss issues
if (workspace.id === WORKSPACE_IDS.SYSTEM_SETTINGS) {
navigate({ to: '/system-settings/general' })
navigate({ to: '/system-settings/site' })
} else {
navigate({ to: '/dashboard' })
}
......
import { type TFunction } from 'i18next'
import {
Box,
CreditCard,
Layout,
Settings,
Shield,
ShieldAlert,
Layout,
Plug,
Box,
Wrench,
} from 'lucide-react'
import { getAuthSectionNavItems } from '@/features/system-settings/auth/section-registry.tsx'
import { getBillingSectionNavItems } from '@/features/system-settings/billing/section-registry.tsx'
import { getContentSectionNavItems } from '@/features/system-settings/content/section-registry.tsx'
import { getGeneralSectionNavItems } from '@/features/system-settings/general/section-registry.tsx'
import { getIntegrationsSectionNavItems } from '@/features/system-settings/integrations/section-registry.tsx'
import { getMaintenanceSectionNavItems } from '@/features/system-settings/maintenance/section-registry.tsx'
import { getModelsSectionNavItems } from '@/features/system-settings/models/section-registry.tsx'
import { getRequestLimitsSectionNavItems } from '@/features/system-settings/request-limits/section-registry.tsx'
import { getOperationsSectionNavItems } from '@/features/system-settings/operations/section-registry.tsx'
import { getSecuritySectionNavItems } from '@/features/system-settings/security/section-registry.tsx'
import { getSiteSectionNavItems } from '@/features/system-settings/site/section-registry.tsx'
import { type NavGroup } from '../types'
/**
......@@ -30,9 +30,9 @@ export function getSystemSettingsNavGroups(t: TFunction): NavGroup[] {
title: t('System Administration'),
items: [
{
title: t('General'),
title: t('Site & Branding'),
icon: Settings,
items: getGeneralSectionNavItems(t),
items: getSiteSectionNavItems(t),
},
{
title: t('Authentication'),
......@@ -40,29 +40,29 @@ export function getSystemSettingsNavGroups(t: TFunction): NavGroup[] {
items: getAuthSectionNavItems(t),
},
{
title: t('Request Limits'),
icon: ShieldAlert,
items: getRequestLimitsSectionNavItems(t),
title: t('Billing & Payment'),
icon: CreditCard,
items: getBillingSectionNavItems(t),
},
{
title: t('Content'),
icon: Layout,
items: getContentSectionNavItems(t),
title: t('Models & Routing'),
icon: Box,
items: getModelsSectionNavItems(t),
},
{
title: t('Integrations'),
icon: Plug,
items: getIntegrationsSectionNavItems(t),
title: t('Security & Limits'),
icon: ShieldAlert,
items: getSecuritySectionNavItems(t),
},
{
title: t('Models'),
icon: Box,
items: getModelsSectionNavItems(t),
title: t('Console Content'),
icon: Layout,
items: getContentSectionNavItems(t),
},
{
title: t('Maintenance'),
title: t('Operations'),
icon: Wrench,
items: getMaintenanceSectionNavItems(t),
items: getOperationsSectionNavItems(t),
},
],
},
......
......@@ -100,8 +100,8 @@ export function ProfileDropdown() {
<DropdownMenuItem
onClick={() =>
navigate({
to: '/system-settings/general',
search: { section: 'system-info' },
to: '/system-settings/site/$section',
params: { section: 'system-info' },
})
}
>
......
......@@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
const FEEDBACK_URL = 'https://github.com/QuantumNous/new-api/issues'
type GeneralErrorProps = React.HTMLAttributes<HTMLDivElement> & {
minimal?: boolean
}
......@@ -28,10 +30,27 @@ export function GeneralError({
{t('Please try again later.')}
</p>
{!minimal && (
<div className='mt-6 flex gap-4'>
<p className='text-muted-foreground text-center text-sm'>
{t('If this keeps happening, please report it on GitHub Issues.')}
</p>
)}
{!minimal && (
<div className='mt-6 flex flex-wrap justify-center gap-4'>
<Button variant='outline' onClick={() => history.go(-1)}>
{t('Go Back')}
</Button>
<Button
variant='outline'
render={
<a
href={FEEDBACK_URL}
target='_blank'
rel='noopener noreferrer'
/>
}
>
{t('Report an issue')}
</Button>
<Button onClick={() => navigate({ to: '/' })}>
{t('Back to Home')}
</Button>
......
......@@ -87,7 +87,10 @@ export function DeploymentAccessGuard({
const navigate = useNavigate()
const handleGoToSettings = () => {
navigate({ to: '/system-settings/integrations' })
navigate({
to: '/system-settings/models/$section',
params: { section: 'model-deployment' },
})
}
// Combined loading state with step indicator
......
......@@ -168,6 +168,13 @@ export function ModelMutateDrawer({
'group_ratio_setting.group_special_usable_group': '{}',
'grok.violation_deduction_enabled': false,
'grok.violation_deduction_amount': 0,
'channel_affinity_setting.enabled': false,
'channel_affinity_setting.switch_on_success': true,
'channel_affinity_setting.max_entries': 100000,
'channel_affinity_setting.default_ttl_seconds': 3600,
'channel_affinity_setting.rules': '[]',
'model_deployment.ionet.api_key': '',
'model_deployment.ionet.enabled': false,
}
return getOptionValue(systemOptionsData.data, defaultModelSettings)
}, [systemOptionsData])
......
import { SettingsPage } from '../components/settings-page'
import type { IntegrationSettings as IntegrationSettingsType } from '../types'
import type { BillingSettings } from '../types'
import {
INTEGRATIONS_DEFAULT_SECTION,
getIntegrationsSectionContent,
BILLING_DEFAULT_SECTION,
getBillingSectionContent,
} from './section-registry.tsx'
const defaultIntegrationSettings: IntegrationSettingsType = {
SMTPServer: '',
SMTPPort: '',
SMTPAccount: '',
SMTPFrom: '',
SMTPToken: '',
SMTPSSLEnabled: false,
SMTPForceAuthLogin: false,
WorkerUrl: '',
WorkerValidKey: '',
WorkerAllowHttpImageRequestEnabled: false,
ChannelDisableThreshold: '',
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,
'model_deployment.ionet.api_key': '',
'model_deployment.ionet.enabled': false,
const defaultBillingSettings: BillingSettings = {
QuotaForNewUser: 0,
PreConsumedQuota: 0,
QuotaForInviter: 0,
QuotaForInvitee: 0,
TopUpLink: '',
'general_setting.docs_link': '',
'quota_setting.enable_free_model_pre_consume': true,
QuotaPerUnit: 500000,
USDExchangeRate: 7,
'general_setting.quota_display_type': 'USD',
'general_setting.custom_currency_symbol': '¤',
'general_setting.custom_currency_exchange_rate': 1,
DisplayInCurrencyEnabled: true,
DisplayTokenStatEnabled: true,
ModelPrice: '',
ModelRatio: '',
CacheRatio: '',
CreateCacheRatio: '',
CompletionRatio: '',
ImageRatio: '',
AudioRatio: '',
AudioCompletionRatio: '',
ExposeRatioEnabled: false,
'billing_setting.billing_mode': '{}',
'billing_setting.billing_expr': '{}',
'tool_price_setting.prices': '{}',
TopupGroupRatio: '',
GroupRatio: '',
UserUsableGroups: '',
GroupGroupRatio: '',
AutoGroups: '',
DefaultUseAutoGroup: false,
'group_ratio_setting.group_special_usable_group': '{}',
PayAddress: '',
EpayId: '',
EpayKey: '',
......@@ -74,15 +85,18 @@ const defaultIntegrationSettings: IntegrationSettingsType = {
WaffoPancakeCurrency: 'USD',
WaffoPancakeUnitPrice: 1,
WaffoPancakeMinTopUp: 1,
'checkin_setting.enabled': false,
'checkin_setting.min_quota': 1000,
'checkin_setting.max_quota': 10000,
}
export function IntegrationSettings() {
export function BillingSettings() {
return (
<SettingsPage
routePath='/_authenticated/system-settings/integrations/$section'
defaultSettings={defaultIntegrationSettings}
defaultSection={INTEGRATIONS_DEFAULT_SECTION}
getSectionContent={getIntegrationsSectionContent}
routePath='/_authenticated/system-settings/billing/$section'
defaultSettings={defaultBillingSettings}
defaultSection={BILLING_DEFAULT_SECTION}
getSectionContent={getBillingSectionContent}
/>
)
}
import { parseCurrencyDisplayType } from '@/lib/currency'
import type { BillingSettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry'
import { CheckinSettingsSection } from '../general/checkin-settings-section'
import { PricingSection } from '../general/pricing-section'
import { QuotaSettingsSection } from '../general/quota-settings-section'
import { PaymentSettingsSection } from '../integrations/payment-settings-section'
import { RatioSettingsCard } from '../models/ratio-settings-card'
const getModelDefaults = (settings: BillingSettings) => ({
ModelPrice: settings.ModelPrice,
ModelRatio: settings.ModelRatio,
CacheRatio: settings.CacheRatio,
CreateCacheRatio: settings.CreateCacheRatio,
CompletionRatio: settings.CompletionRatio,
ImageRatio: settings.ImageRatio,
AudioRatio: settings.AudioRatio,
AudioCompletionRatio: settings.AudioCompletionRatio,
ExposeRatioEnabled: settings.ExposeRatioEnabled,
BillingMode: settings['billing_setting.billing_mode'],
BillingExpr: settings['billing_setting.billing_expr'],
})
const getGroupDefaults = (settings: BillingSettings) => ({
TopupGroupRatio: settings.TopupGroupRatio,
GroupRatio: settings.GroupRatio,
UserUsableGroups: settings.UserUsableGroups,
GroupGroupRatio: settings.GroupGroupRatio,
AutoGroups: settings.AutoGroups,
DefaultUseAutoGroup: settings.DefaultUseAutoGroup,
GroupSpecialUsableGroup:
settings['group_ratio_setting.group_special_usable_group'],
})
const BILLING_SECTIONS = [
{
id: 'quota',
titleKey: 'Quota Settings',
descriptionKey: 'Configure user quota allocation and rewards',
build: (settings: BillingSettings) => (
<QuotaSettingsSection
defaultValues={{
QuotaForNewUser: settings.QuotaForNewUser,
PreConsumedQuota: settings.PreConsumedQuota,
QuotaForInviter: settings.QuotaForInviter,
QuotaForInvitee: settings.QuotaForInvitee,
TopUpLink: settings.TopUpLink,
'general_setting.docs_link': settings['general_setting.docs_link'],
'quota_setting.enable_free_model_pre_consume':
settings['quota_setting.enable_free_model_pre_consume'],
}}
/>
),
},
{
id: 'currency',
titleKey: 'Currency & Display',
descriptionKey: 'Configure currency conversion and quota display options',
build: (settings: BillingSettings) => (
<PricingSection
defaultValues={{
QuotaPerUnit: settings.QuotaPerUnit,
USDExchangeRate: settings.USDExchangeRate,
DisplayInCurrencyEnabled: settings.DisplayInCurrencyEnabled,
DisplayTokenStatEnabled: settings.DisplayTokenStatEnabled,
general_setting: {
quota_display_type: parseCurrencyDisplayType(
settings['general_setting.quota_display_type']
),
custom_currency_symbol:
settings['general_setting.custom_currency_symbol'] ?? '¤',
custom_currency_exchange_rate:
settings['general_setting.custom_currency_exchange_rate'] ?? 1,
},
}}
/>
),
},
{
id: 'model-pricing',
titleKey: 'Model Pricing',
descriptionKey: 'Configure model pricing ratios and tool prices',
build: (settings: BillingSettings) => (
<RatioSettingsCard
titleKey='Model Pricing'
descriptionKey='Configure model pricing ratios and tool prices'
modelDefaults={getModelDefaults(settings)}
groupDefaults={getGroupDefaults(settings)}
toolPricesDefault={settings['tool_price_setting.prices']}
visibleTabs={['models', 'tool-prices', 'upstream-sync']}
/>
),
},
{
id: 'group-pricing',
titleKey: 'Group Pricing',
descriptionKey: 'Configure group ratios and group-specific pricing rules',
build: (settings: BillingSettings) => (
<RatioSettingsCard
titleKey='Group Pricing'
descriptionKey='Configure group ratios and group-specific pricing rules'
modelDefaults={getModelDefaults(settings)}
groupDefaults={getGroupDefaults(settings)}
toolPricesDefault={settings['tool_price_setting.prices']}
visibleTabs={['groups']}
/>
),
},
{
id: 'payment',
titleKey: 'Payment Gateway',
descriptionKey: 'Configure payment gateway integrations',
build: (settings: BillingSettings) => (
<PaymentSettingsSection
defaultValues={{
PayAddress: settings.PayAddress,
EpayId: settings.EpayId,
EpayKey: settings.EpayKey,
Price: settings.Price,
MinTopUp: settings.MinTopUp,
CustomCallbackAddress: settings.CustomCallbackAddress,
PayMethods: settings.PayMethods,
AmountOptions: settings['payment_setting.amount_options'],
AmountDiscount: settings['payment_setting.amount_discount'],
StripeApiSecret: settings.StripeApiSecret,
StripeWebhookSecret: settings.StripeWebhookSecret,
StripePriceId: settings.StripePriceId,
StripeUnitPrice: settings.StripeUnitPrice,
StripeMinTopUp: settings.StripeMinTopUp,
StripePromotionCodesEnabled: settings.StripePromotionCodesEnabled,
CreemApiKey: settings.CreemApiKey,
CreemWebhookSecret: settings.CreemWebhookSecret,
CreemTestMode: settings.CreemTestMode,
CreemProducts: settings.CreemProducts,
}}
waffoDefaultValues={{
WaffoEnabled: settings.WaffoEnabled ?? false,
WaffoApiKey: settings.WaffoApiKey ?? '',
WaffoPrivateKey: settings.WaffoPrivateKey ?? '',
WaffoPublicCert: settings.WaffoPublicCert ?? '',
WaffoSandboxPublicCert: settings.WaffoSandboxPublicCert ?? '',
WaffoSandboxApiKey: settings.WaffoSandboxApiKey ?? '',
WaffoSandboxPrivateKey: settings.WaffoSandboxPrivateKey ?? '',
WaffoSandbox: settings.WaffoSandbox ?? false,
WaffoMerchantId: settings.WaffoMerchantId ?? '',
WaffoCurrency: settings.WaffoCurrency ?? 'USD',
WaffoUnitPrice: settings.WaffoUnitPrice ?? 1,
WaffoMinTopUp: settings.WaffoMinTopUp ?? 1,
WaffoNotifyUrl: settings.WaffoNotifyUrl ?? '',
WaffoReturnUrl: settings.WaffoReturnUrl ?? '',
WaffoPayMethods: settings.WaffoPayMethods ?? '[]',
}}
waffoPancakeDefaultValues={{
WaffoPancakeEnabled: settings.WaffoPancakeEnabled ?? false,
WaffoPancakeSandbox: settings.WaffoPancakeSandbox ?? false,
WaffoPancakeMerchantID: settings.WaffoPancakeMerchantID ?? '',
WaffoPancakePrivateKey: settings.WaffoPancakePrivateKey ?? '',
WaffoPancakeWebhookPublicKey:
settings.WaffoPancakeWebhookPublicKey ?? '',
WaffoPancakeWebhookTestKey:
settings.WaffoPancakeWebhookTestKey ?? '',
WaffoPancakeStoreID: settings.WaffoPancakeStoreID ?? '',
WaffoPancakeProductID: settings.WaffoPancakeProductID ?? '',
WaffoPancakeReturnURL: settings.WaffoPancakeReturnURL ?? '',
WaffoPancakeCurrency: settings.WaffoPancakeCurrency ?? 'USD',
WaffoPancakeUnitPrice: settings.WaffoPancakeUnitPrice ?? 1,
WaffoPancakeMinTopUp: settings.WaffoPancakeMinTopUp ?? 1,
}}
/>
),
},
{
id: 'checkin',
titleKey: 'Check-in Rewards',
descriptionKey: 'Configure daily check-in rewards for users',
build: (settings: BillingSettings) => (
<CheckinSettingsSection
defaultValues={{
enabled: settings['checkin_setting.enabled'],
minQuota: settings['checkin_setting.min_quota'],
maxQuota: settings['checkin_setting.max_quota'],
}}
/>
),
},
] as const
export type BillingSectionId = (typeof BILLING_SECTIONS)[number]['id']
const billingRegistry = createSectionRegistry<BillingSectionId, BillingSettings>(
{
sections: BILLING_SECTIONS,
defaultSection: 'quota',
basePath: '/system-settings/billing',
urlStyle: 'path',
}
)
export const BILLING_SECTION_IDS = billingRegistry.sectionIds
export const BILLING_DEFAULT_SECTION = billingRegistry.defaultSection
export const getBillingSectionNavItems = billingRegistry.getSectionNavItems
export const getBillingSectionContent = billingRegistry.getSectionContent
import { useParams } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { parseCurrencyDisplayType } from '@/lib/currency'
import { useSystemOptions, getOptionValue } from '../hooks/use-system-options'
import type { GeneralSettings } from '../types'
import {
GENERAL_DEFAULT_SECTION,
getGeneralSectionContent,
} from './section-registry.tsx'
const defaultGeneralSettings: GeneralSettings = {
'theme.frontend': 'default',
Notice: '',
SystemName: 'New API',
Logo: '',
Footer: '',
About: '',
HomePageContent: '',
ServerAddress: '',
'legal.user_agreement': '',
'legal.privacy_policy': '',
QuotaForNewUser: 0,
PreConsumedQuota: 0,
QuotaForInviter: 0,
QuotaForInvitee: 0,
TopUpLink: '',
'general_setting.docs_link': '',
'quota_setting.enable_free_model_pre_consume': true,
QuotaPerUnit: 500000,
USDExchangeRate: 7,
'general_setting.quota_display_type': 'USD',
'general_setting.custom_currency_symbol': '¤',
'general_setting.custom_currency_exchange_rate': 1,
RetryTimes: 0,
DisplayInCurrencyEnabled: true,
DisplayTokenStatEnabled: true,
DefaultCollapseSidebar: false,
DemoSiteEnabled: false,
SelfUseModeEnabled: false,
'checkin_setting.enabled': false,
'checkin_setting.min_quota': 1000,
'checkin_setting.max_quota': 10000,
'channel_affinity_setting.enabled': false,
'channel_affinity_setting.switch_on_success': true,
'channel_affinity_setting.max_entries': 100000,
'channel_affinity_setting.default_ttl_seconds': 3600,
'channel_affinity_setting.rules': '[]',
}
export function GeneralSettings() {
const { t } = useTranslation()
const { data, isLoading } = useSystemOptions()
const params = useParams({
from: '/_authenticated/system-settings/general/$section',
})
if (isLoading) {
return (
<div className='flex items-center justify-center py-12'>
<div className='text-muted-foreground'>{t('Loading settings...')}</div>
</div>
)
}
const settings = getOptionValue(data?.data, defaultGeneralSettings)
const quotaDisplayType = parseCurrencyDisplayType(
settings['general_setting.quota_display_type']
)
const activeSection = (params?.section ?? GENERAL_DEFAULT_SECTION) as
| 'system-info'
| 'quota'
| 'pricing'
| 'checkin'
| 'behavior'
| 'channel-affinity'
const sectionContent = getGeneralSectionContent(
activeSection,
settings,
quotaDisplayType
)
return (
<div className='flex h-full w-full flex-1 flex-col'>
<div className='faded-bottom h-full w-full overflow-y-auto scroll-smooth pe-4 pb-12'>
<div className='space-y-4'>{sectionContent}</div>
</div>
</div>
)
}
import type { GeneralSettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry'
import { ChannelAffinitySection } from './channel-affinity'
import { CheckinSettingsSection } from './checkin-settings-section'
import { PricingSection } from './pricing-section'
import { QuotaSettingsSection } from './quota-settings-section'
import { SystemBehaviorSection } from './system-behavior-section'
import { SystemInfoSection } from './system-info-section'
const GENERAL_SECTIONS = [
{
id: 'system-info',
titleKey: 'System Information',
descriptionKey: 'Configure basic system information and branding',
build: (settings: GeneralSettings) => (
<SystemInfoSection
defaultValues={{
theme: {
frontend: settings['theme.frontend'] as 'default' | 'classic',
},
Notice: settings.Notice,
SystemName: settings.SystemName,
Logo: settings.Logo,
Footer: settings.Footer,
About: settings.About,
HomePageContent: settings.HomePageContent,
ServerAddress: settings.ServerAddress,
legal: {
user_agreement: settings['legal.user_agreement'],
privacy_policy: settings['legal.privacy_policy'],
},
}}
/>
),
},
{
id: 'quota',
titleKey: 'Quota Settings',
descriptionKey: 'Configure user quota allocation and rewards',
build: (settings: GeneralSettings) => (
<QuotaSettingsSection
defaultValues={{
QuotaForNewUser: settings.QuotaForNewUser,
PreConsumedQuota: settings.PreConsumedQuota,
QuotaForInviter: settings.QuotaForInviter,
QuotaForInvitee: settings.QuotaForInvitee,
TopUpLink: settings.TopUpLink,
'general_setting.docs_link': settings['general_setting.docs_link'],
'quota_setting.enable_free_model_pre_consume':
settings['quota_setting.enable_free_model_pre_consume'],
}}
/>
),
},
{
id: 'pricing',
titleKey: 'Pricing & Display',
descriptionKey: 'Configure pricing model and display options',
build: (
settings: GeneralSettings,
quotaDisplayType: 'USD' | 'CNY' | 'TOKENS' | 'CUSTOM'
) => (
<PricingSection
defaultValues={{
QuotaPerUnit: settings.QuotaPerUnit,
USDExchangeRate: settings.USDExchangeRate,
DisplayInCurrencyEnabled: settings.DisplayInCurrencyEnabled,
DisplayTokenStatEnabled: settings.DisplayTokenStatEnabled,
general_setting: {
quota_display_type: quotaDisplayType,
custom_currency_symbol:
settings['general_setting.custom_currency_symbol'] ?? '¤',
custom_currency_exchange_rate:
settings['general_setting.custom_currency_exchange_rate'] ?? 1,
},
}}
/>
),
},
{
id: 'checkin',
titleKey: 'Check-in Settings',
descriptionKey: 'Configure daily check-in rewards for users',
build: (settings: GeneralSettings) => (
<CheckinSettingsSection
defaultValues={{
enabled: settings['checkin_setting.enabled'],
minQuota: settings['checkin_setting.min_quota'],
maxQuota: settings['checkin_setting.max_quota'],
}}
/>
),
},
{
id: 'behavior',
titleKey: 'System Behavior',
descriptionKey: 'Configure system-wide behavior and defaults',
build: (settings: GeneralSettings) => (
<SystemBehaviorSection
defaultValues={{
RetryTimes: settings.RetryTimes,
DefaultCollapseSidebar: settings.DefaultCollapseSidebar,
DemoSiteEnabled: settings.DemoSiteEnabled,
SelfUseModeEnabled: settings.SelfUseModeEnabled,
}}
/>
),
},
{
id: 'channel-affinity',
titleKey: 'Channel Affinity',
descriptionKey: 'Configure channel affinity (sticky routing) rules',
build: (settings: GeneralSettings) => (
<ChannelAffinitySection
defaultValues={{
'channel_affinity_setting.enabled':
settings['channel_affinity_setting.enabled'],
'channel_affinity_setting.switch_on_success':
settings['channel_affinity_setting.switch_on_success'],
'channel_affinity_setting.max_entries':
settings['channel_affinity_setting.max_entries'],
'channel_affinity_setting.default_ttl_seconds':
settings['channel_affinity_setting.default_ttl_seconds'],
'channel_affinity_setting.rules':
settings['channel_affinity_setting.rules'],
}}
/>
),
},
] as const
export type GeneralSectionId = (typeof GENERAL_SECTIONS)[number]['id']
const generalRegistry = createSectionRegistry<
GeneralSectionId,
GeneralSettings,
['USD' | 'CNY' | 'TOKENS' | 'CUSTOM']
>({
sections: GENERAL_SECTIONS,
defaultSection: 'system-info',
basePath: '/system-settings/general',
urlStyle: 'path',
})
export const GENERAL_SECTION_IDS = generalRegistry.sectionIds
export const GENERAL_DEFAULT_SECTION = generalRegistry.defaultSection
export const getGeneralSectionNavItems = generalRegistry.getSectionNavItems
export const getGeneralSectionContent = generalRegistry.getSectionContent
......@@ -32,7 +32,6 @@ const _systemInfoSchema = z.object({
theme: z.object({
frontend: z.enum(['default', 'classic']),
}),
Notice: z.string().optional(),
SystemName: z.string().min(1),
ServerAddress: z.string().optional(),
Logo: z.string().url().optional().or(z.literal('')),
......@@ -65,7 +64,6 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) {
frontend:
defaultValues.theme?.frontend === 'classic' ? 'classic' : 'default',
},
Notice: normalizeValue(defaultValues.Notice),
SystemName: normalizeValue(defaultValues.SystemName),
ServerAddress: normalizeValue(defaultValues.ServerAddress),
Logo: normalizeValue(defaultValues.Logo),
......@@ -82,7 +80,6 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) {
theme: z.object({
frontend: z.enum(['default', 'classic']),
}),
Notice: z.string().optional(),
SystemName: z.string().min(1, {
error: () => t('System name is required'),
}),
......@@ -163,31 +160,6 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) {
<FormField
control={form.control}
name='Notice'
render={({ field }) => (
<FormItem>
<FormLabel>{t('Notice')}</FormLabel>
<FormControl>
<Textarea
placeholder={t(
'Enter announcement content (supports Markdown & HTML)'
)}
rows={6}
{...field}
/>
</FormControl>
<FormDescription>
{t(
'Announcement displayed to users (supports Markdown & HTML)'
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name='SystemName'
render={({ field }) => (
<FormItem>
......
import type { IntegrationSettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry'
import { EmailSettingsSection } from './email-settings-section'
import { IoNetDeploymentSettingsSection } from './ionet-deployment-settings-section'
import { MonitoringSettingsSection } from './monitoring-settings-section'
import { PaymentSettingsSection } from './payment-settings-section'
import { WorkerSettingsSection } from './worker-settings-section'
const INTEGRATIONS_SECTIONS = [
{
id: 'payment',
titleKey: 'Payment Gateway',
descriptionKey: 'Configure payment gateway integrations',
build: (settings: IntegrationSettings) => (
<PaymentSettingsSection
defaultValues={{
PayAddress: settings.PayAddress,
EpayId: settings.EpayId,
EpayKey: settings.EpayKey,
Price: settings.Price,
MinTopUp: settings.MinTopUp,
CustomCallbackAddress: settings.CustomCallbackAddress,
PayMethods: settings.PayMethods,
AmountOptions: settings['payment_setting.amount_options'],
AmountDiscount: settings['payment_setting.amount_discount'],
StripeApiSecret: settings.StripeApiSecret,
StripeWebhookSecret: settings.StripeWebhookSecret,
StripePriceId: settings.StripePriceId,
StripeUnitPrice: settings.StripeUnitPrice,
StripeMinTopUp: settings.StripeMinTopUp,
StripePromotionCodesEnabled: settings.StripePromotionCodesEnabled,
CreemApiKey: settings.CreemApiKey,
CreemWebhookSecret: settings.CreemWebhookSecret,
CreemTestMode: settings.CreemTestMode,
CreemProducts: settings.CreemProducts,
}}
waffoDefaultValues={{
WaffoEnabled: settings.WaffoEnabled ?? false,
WaffoApiKey: settings.WaffoApiKey ?? '',
WaffoPrivateKey: settings.WaffoPrivateKey ?? '',
WaffoPublicCert: settings.WaffoPublicCert ?? '',
WaffoSandboxPublicCert: settings.WaffoSandboxPublicCert ?? '',
WaffoSandboxApiKey: settings.WaffoSandboxApiKey ?? '',
WaffoSandboxPrivateKey: settings.WaffoSandboxPrivateKey ?? '',
WaffoSandbox: settings.WaffoSandbox ?? false,
WaffoMerchantId: settings.WaffoMerchantId ?? '',
WaffoCurrency: settings.WaffoCurrency ?? 'USD',
WaffoUnitPrice: settings.WaffoUnitPrice ?? 1,
WaffoMinTopUp: settings.WaffoMinTopUp ?? 1,
WaffoNotifyUrl: settings.WaffoNotifyUrl ?? '',
WaffoReturnUrl: settings.WaffoReturnUrl ?? '',
WaffoPayMethods: settings.WaffoPayMethods ?? '[]',
}}
waffoPancakeDefaultValues={{
WaffoPancakeEnabled: settings.WaffoPancakeEnabled ?? false,
WaffoPancakeSandbox: settings.WaffoPancakeSandbox ?? false,
WaffoPancakeMerchantID: settings.WaffoPancakeMerchantID ?? '',
WaffoPancakePrivateKey: settings.WaffoPancakePrivateKey ?? '',
WaffoPancakeWebhookPublicKey:
settings.WaffoPancakeWebhookPublicKey ?? '',
WaffoPancakeWebhookTestKey: settings.WaffoPancakeWebhookTestKey ?? '',
WaffoPancakeStoreID: settings.WaffoPancakeStoreID ?? '',
WaffoPancakeProductID: settings.WaffoPancakeProductID ?? '',
WaffoPancakeReturnURL: settings.WaffoPancakeReturnURL ?? '',
WaffoPancakeCurrency: settings.WaffoPancakeCurrency ?? 'USD',
WaffoPancakeUnitPrice: settings.WaffoPancakeUnitPrice ?? 1,
WaffoPancakeMinTopUp: settings.WaffoPancakeMinTopUp ?? 1,
}}
/>
),
},
{
id: 'email',
titleKey: 'SMTP Email',
descriptionKey: 'Configure SMTP email settings',
build: (settings: IntegrationSettings) => (
<EmailSettingsSection
defaultValues={{
SMTPServer: settings.SMTPServer,
SMTPPort: settings.SMTPPort,
SMTPAccount: settings.SMTPAccount,
SMTPFrom: settings.SMTPFrom,
SMTPToken: settings.SMTPToken,
SMTPSSLEnabled: settings.SMTPSSLEnabled,
SMTPForceAuthLogin: settings.SMTPForceAuthLogin,
}}
/>
),
},
{
id: 'worker',
titleKey: 'Worker Proxy',
descriptionKey: 'Configure worker service settings',
build: (settings: IntegrationSettings) => (
<WorkerSettingsSection
defaultValues={{
WorkerUrl: settings.WorkerUrl,
WorkerValidKey: settings.WorkerValidKey,
WorkerAllowHttpImageRequestEnabled:
settings.WorkerAllowHttpImageRequestEnabled,
}}
/>
),
},
{
id: 'ionet',
titleKey: 'io.net Deployments',
descriptionKey: 'Configure IoNet model deployment settings',
build: (settings: IntegrationSettings) => (
<IoNetDeploymentSettingsSection
defaultValues={{
enabled: settings['model_deployment.ionet.enabled'],
apiKey: settings['model_deployment.ionet.api_key'],
}}
/>
),
},
{
id: 'monitoring',
titleKey: 'Monitoring & Alerts',
descriptionKey: 'Configure channel monitoring and automation',
build: (settings: IntegrationSettings) => (
<MonitoringSettingsSection
defaultValues={{
ChannelDisableThreshold: settings.ChannelDisableThreshold,
QuotaRemindThreshold: settings.QuotaRemindThreshold,
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'],
}}
/>
),
},
] as const
export type IntegrationSectionId = (typeof INTEGRATIONS_SECTIONS)[number]['id']
const integrationsRegistry = createSectionRegistry<
IntegrationSectionId,
IntegrationSettings
>({
sections: INTEGRATIONS_SECTIONS,
defaultSection: 'payment',
basePath: '/system-settings/integrations',
urlStyle: 'path',
})
export const INTEGRATIONS_SECTION_IDS = integrationsRegistry.sectionIds
export const INTEGRATIONS_DEFAULT_SECTION = integrationsRegistry.defaultSection
export const getIntegrationsSectionNavItems =
integrationsRegistry.getSectionNavItems
export const getIntegrationsSectionContent =
integrationsRegistry.getSectionContent
import type { MaintenanceSettings } from '../types'
export type HeaderNavPricingConfig = {
enabled: boolean
requireAuth: boolean
......@@ -62,25 +60,6 @@ export const SIDEBAR_MODULES_DEFAULT: SidebarModulesAdminConfig = {
},
}
export const DEFAULT_MAINTENANCE_SETTINGS: MaintenanceSettings = {
Notice: '',
LogConsumeEnabled: false,
HeaderNavModules: JSON.stringify(HEADER_NAV_DEFAULT),
SidebarModulesAdmin: JSON.stringify(SIDEBAR_MODULES_DEFAULT),
'performance_setting.disk_cache_enabled': false,
'performance_setting.disk_cache_threshold_mb': 10,
'performance_setting.disk_cache_max_size_mb': 1024,
'performance_setting.disk_cache_path': '',
'performance_setting.monitor_enabled': false,
'performance_setting.monitor_cpu_threshold': 90,
'performance_setting.monitor_memory_threshold': 90,
'performance_setting.monitor_disk_threshold': 95,
'perf_metrics_setting.enabled': true,
'perf_metrics_setting.flush_interval': 5,
'perf_metrics_setting.bucket_time': 'hour',
'perf_metrics_setting.retention_days': 0,
}
const toBoolean = (value: unknown, fallback: boolean): boolean => {
if (typeof value === 'boolean') return value
if (typeof value === 'number') return value === 1
......
import type { MaintenanceSettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry'
import {
parseHeaderNavModules,
parseSidebarModulesAdmin,
serializeHeaderNavModules,
serializeSidebarModulesAdmin,
} from './config'
import { HeaderNavigationSection } from './header-navigation-section'
import { LogSettingsSection } from './log-settings-section'
import { NoticeSection } from './notice-section'
import { PerformanceSection } from './performance-section'
import { SidebarModulesSection } from './sidebar-modules-section'
import { UpdateCheckerSection } from './update-checker-section'
const MAINTENANCE_SECTIONS = [
{
id: 'update-checker',
titleKey: 'System maintenance',
descriptionKey: 'Check for system updates',
build: (
_settings: MaintenanceSettings,
currentVersion?: string | null,
startTime?: number | null
) => (
<UpdateCheckerSection
currentVersion={currentVersion}
startTime={startTime}
/>
),
},
{
id: 'notice',
titleKey: 'System Notice',
descriptionKey: 'Configure system maintenance notice',
build: (settings: MaintenanceSettings) => (
<NoticeSection defaultValue={settings.Notice ?? ''} />
),
},
{
id: 'logs',
titleKey: 'Log Maintenance',
descriptionKey: 'Configure log consumption settings',
build: (settings: MaintenanceSettings) => (
<LogSettingsSection
defaultEnabled={Boolean(settings.LogConsumeEnabled)}
/>
),
},
{
id: 'header-navigation',
titleKey: 'Header navigation',
descriptionKey: 'Configure header navigation modules',
build: (settings: MaintenanceSettings) => {
const headerNavConfig = parseHeaderNavModules(settings.HeaderNavModules)
const headerNavSerialized = serializeHeaderNavModules(headerNavConfig)
return (
<HeaderNavigationSection
config={headerNavConfig}
initialSerialized={headerNavSerialized}
/>
)
},
},
{
id: 'sidebar-modules',
titleKey: 'Sidebar modules',
descriptionKey: 'Configure sidebar modules for admin',
build: (settings: MaintenanceSettings) => {
const sidebarConfig = parseSidebarModulesAdmin(
settings.SidebarModulesAdmin
)
const sidebarSerialized = serializeSidebarModulesAdmin(sidebarConfig)
return (
<SidebarModulesSection
config={sidebarConfig}
initialSerialized={sidebarSerialized}
/>
)
},
},
{
id: 'performance',
titleKey: 'Performance',
descriptionKey: 'Disk cache, system monitoring and performance stats',
build: (settings: MaintenanceSettings) => (
<PerformanceSection
defaultValues={{
'performance_setting.disk_cache_enabled':
settings['performance_setting.disk_cache_enabled'] ?? false,
'performance_setting.disk_cache_threshold_mb':
settings['performance_setting.disk_cache_threshold_mb'] ?? 10,
'performance_setting.disk_cache_max_size_mb':
settings['performance_setting.disk_cache_max_size_mb'] ?? 1024,
'performance_setting.disk_cache_path':
settings['performance_setting.disk_cache_path'] ?? '',
'performance_setting.monitor_enabled':
settings['performance_setting.monitor_enabled'] ?? false,
'performance_setting.monitor_cpu_threshold':
settings['performance_setting.monitor_cpu_threshold'] ?? 90,
'performance_setting.monitor_memory_threshold':
settings['performance_setting.monitor_memory_threshold'] ?? 90,
'performance_setting.monitor_disk_threshold':
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,
}}
/>
),
},
] as const
export type MaintenanceSectionId = (typeof MAINTENANCE_SECTIONS)[number]['id']
const maintenanceRegistry = createSectionRegistry<
MaintenanceSectionId,
MaintenanceSettings,
[string | null | undefined, number | null | undefined]
>({
sections: MAINTENANCE_SECTIONS,
defaultSection: 'update-checker',
basePath: '/system-settings/maintenance',
urlStyle: 'path',
})
export const MAINTENANCE_SECTION_IDS = maintenanceRegistry.sectionIds
export const MAINTENANCE_DEFAULT_SECTION = maintenanceRegistry.defaultSection
export const getMaintenanceSectionNavItems =
maintenanceRegistry.getSectionNavItems
export const getMaintenanceSectionContent =
maintenanceRegistry.getSectionContent
......@@ -43,6 +43,13 @@ const defaultModelSettings: ModelSettings = {
AutoGroups: '',
DefaultUseAutoGroup: false,
'group_ratio_setting.group_special_usable_group': '{}',
'channel_affinity_setting.enabled': false,
'channel_affinity_setting.switch_on_success': true,
'channel_affinity_setting.max_entries': 100000,
'channel_affinity_setting.default_ttl_seconds': 3600,
'channel_affinity_setting.rules': '[]',
'model_deployment.ionet.api_key': '',
'model_deployment.ionet.enabled': false,
}
export function ModelSettings() {
......
......@@ -179,17 +179,24 @@ const groupSchema = z.object({
type ModelFormValues = z.infer<typeof modelSchema>
type GroupFormValues = z.infer<typeof groupSchema>
type RatioTabId = 'models' | 'groups' | 'tool-prices' | 'upstream-sync'
type RatioSettingsCardProps = {
modelDefaults: ModelFormValues
groupDefaults: GroupFormValues
toolPricesDefault: string
titleKey?: string
descriptionKey?: string
visibleTabs?: RatioTabId[]
}
export function RatioSettingsCard({
modelDefaults,
groupDefaults,
toolPricesDefault,
titleKey = 'Pricing Ratios',
descriptionKey = 'Configure model, caching, and group ratios used for billing',
visibleTabs = ['models', 'groups', 'tool-prices', 'upstream-sync'],
}: RatioSettingsCardProps) {
const { t } = useTranslation()
const updateOption = useUpdateOption()
......@@ -414,61 +421,79 @@ export function RatioSettingsCard({
resetMutate()
}, [resetMutate])
return (
<SettingsSection
title={t('Pricing Ratios')}
description={t(
'Configure model, caching, and group ratios used for billing'
)}
>
<Tabs defaultValue='models' className='space-y-6'>
<TabsList className='grid w-full grid-cols-4'>
<TabsTrigger value='models'>{t('Model ratios')}</TabsTrigger>
<TabsTrigger value='groups'>{t('Group ratios')}</TabsTrigger>
<TabsTrigger value='tool-prices'>{t('Tool prices')}</TabsTrigger>
<TabsTrigger value='upstream-sync'>
{t('Upstream price sync')}
</TabsTrigger>
</TabsList>
<TabsContent value='models'>
<ModelRatioForm
form={modelForm}
onSave={saveModelRatios}
onReset={handleResetRatios}
isSaving={updateOption.isPending}
isResetting={resetMutation.isPending}
/>
</TabsContent>
const tabLabels: Record<RatioTabId, string> = {
models: 'Model ratios',
groups: 'Group ratios',
'tool-prices': 'Tool prices',
'upstream-sync': 'Upstream price sync',
}
const tabsGridClass =
{
1: 'grid-cols-1',
2: 'grid-cols-2',
3: 'grid-cols-3',
4: 'grid-cols-4',
}[visibleTabs.length] ?? 'grid-cols-4'
const defaultTab = visibleTabs[0] ?? 'models'
<TabsContent value='groups'>
<GroupRatioForm
form={groupForm}
onSave={saveGroupRatios}
isSaving={updateOption.isPending}
/>
</TabsContent>
const renderTabContent = (tab: RatioTabId) => {
if (tab === 'models') {
return (
<ModelRatioForm
form={modelForm}
onSave={saveModelRatios}
onReset={handleResetRatios}
isSaving={updateOption.isPending}
isResetting={resetMutation.isPending}
/>
)
}
if (tab === 'groups') {
return (
<GroupRatioForm
form={groupForm}
onSave={saveGroupRatios}
isSaving={updateOption.isPending}
/>
)
}
if (tab === 'tool-prices') {
return <ToolPriceSettings defaultValue={toolPricesDefault} />
}
return (
<UpstreamRatioSync
modelRatios={{
ModelPrice: modelDefaults.ModelPrice,
ModelRatio: modelDefaults.ModelRatio,
CompletionRatio: modelDefaults.CompletionRatio,
CacheRatio: modelDefaults.CacheRatio,
CreateCacheRatio: modelDefaults.CreateCacheRatio,
ImageRatio: modelDefaults.ImageRatio,
AudioRatio: modelDefaults.AudioRatio,
AudioCompletionRatio: modelDefaults.AudioCompletionRatio,
'billing_setting.billing_mode': modelDefaults.BillingMode,
'billing_setting.billing_expr': modelDefaults.BillingExpr,
}}
/>
)
}
<TabsContent value='tool-prices'>
<ToolPriceSettings defaultValue={toolPricesDefault} />
</TabsContent>
return (
<SettingsSection title={t(titleKey)} description={t(descriptionKey)}>
<Tabs defaultValue={defaultTab} className='space-y-6'>
<TabsList className={`grid w-full ${tabsGridClass}`}>
{visibleTabs.map((tab) => (
<TabsTrigger key={tab} value={tab}>
{t(tabLabels[tab])}
</TabsTrigger>
))}
</TabsList>
<TabsContent value='upstream-sync'>
<UpstreamRatioSync
modelRatios={{
ModelPrice: modelDefaults.ModelPrice,
ModelRatio: modelDefaults.ModelRatio,
CompletionRatio: modelDefaults.CompletionRatio,
CacheRatio: modelDefaults.CacheRatio,
CreateCacheRatio: modelDefaults.CreateCacheRatio,
ImageRatio: modelDefaults.ImageRatio,
AudioRatio: modelDefaults.AudioRatio,
AudioCompletionRatio: modelDefaults.AudioCompletionRatio,
'billing_setting.billing_mode': modelDefaults.BillingMode,
'billing_setting.billing_expr': modelDefaults.BillingExpr,
}}
/>
</TabsContent>
{visibleTabs.map((tab) => (
<TabsContent key={tab} value={tab}>
{renderTabContent(tab)}
</TabsContent>
))}
</Tabs>
<ConfirmDialog
......
......@@ -4,7 +4,8 @@ import { ClaudeSettingsCard } from './claude-settings-card'
import { GeminiSettingsCard } from './gemini-settings-card'
import { GlobalSettingsCard } from './global-settings-card'
import { GrokSettingsCard } from './grok-settings-card'
import { RatioSettingsCard } from './ratio-settings-card'
import { ChannelAffinitySection } from '../general/channel-affinity'
import { IoNetDeploymentSettingsSection } from '../integrations/ionet-deployment-settings-section'
function formatJsonForEditor(value: string, fallback: string) {
const raw = (value ?? '').toString().trim()
......@@ -106,34 +107,35 @@ const MODELS_SECTIONS = [
),
},
{
id: 'ratio',
titleKey: 'Pricing Ratios',
descriptionKey: 'Configure model pricing and ratio settings',
id: 'channel-affinity',
titleKey: 'Channel Affinity',
descriptionKey: 'Configure channel affinity (sticky routing) rules',
build: (settings: ModelSettings) => (
<RatioSettingsCard
modelDefaults={{
ModelPrice: settings.ModelPrice,
ModelRatio: settings.ModelRatio,
CacheRatio: settings.CacheRatio,
CreateCacheRatio: settings.CreateCacheRatio,
CompletionRatio: settings.CompletionRatio,
ImageRatio: settings.ImageRatio,
AudioRatio: settings.AudioRatio,
AudioCompletionRatio: settings.AudioCompletionRatio,
ExposeRatioEnabled: settings.ExposeRatioEnabled,
BillingMode: settings['billing_setting.billing_mode'],
BillingExpr: settings['billing_setting.billing_expr'],
<ChannelAffinitySection
defaultValues={{
'channel_affinity_setting.enabled':
settings['channel_affinity_setting.enabled'],
'channel_affinity_setting.switch_on_success':
settings['channel_affinity_setting.switch_on_success'],
'channel_affinity_setting.max_entries':
settings['channel_affinity_setting.max_entries'],
'channel_affinity_setting.default_ttl_seconds':
settings['channel_affinity_setting.default_ttl_seconds'],
'channel_affinity_setting.rules':
settings['channel_affinity_setting.rules'],
}}
toolPricesDefault={settings['tool_price_setting.prices']}
groupDefaults={{
TopupGroupRatio: settings.TopupGroupRatio,
GroupRatio: settings.GroupRatio,
UserUsableGroups: settings.UserUsableGroups,
GroupGroupRatio: settings.GroupGroupRatio,
AutoGroups: settings.AutoGroups,
DefaultUseAutoGroup: settings.DefaultUseAutoGroup,
GroupSpecialUsableGroup:
settings['group_ratio_setting.group_special_usable_group'],
/>
),
},
{
id: 'model-deployment',
titleKey: 'Model Deployment',
descriptionKey: 'Configure model deployment provider settings',
build: (settings: ModelSettings) => (
<IoNetDeploymentSettingsSection
defaultValues={{
enabled: settings['model_deployment.ionet.enabled'],
apiKey: settings['model_deployment.ionet.api_key'],
}}
/>
),
......
......@@ -3,22 +3,62 @@ import { useParams } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { useStatus } from '@/hooks/use-status'
import { getOptionValue, useSystemOptions } from '../hooks/use-system-options'
import { DEFAULT_MAINTENANCE_SETTINGS } from './config'
import type { OperationsSettings } from '../types'
import {
MAINTENANCE_DEFAULT_SECTION,
getMaintenanceSectionContent,
OPERATIONS_DEFAULT_SECTION,
getOperationsSectionContent,
} from './section-registry.tsx'
export function MaintenanceSettings() {
const defaultOperationsSettings: OperationsSettings = {
RetryTimes: 0,
DefaultCollapseSidebar: false,
DemoSiteEnabled: false,
SelfUseModeEnabled: false,
ChannelDisableThreshold: '',
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: '',
SMTPPort: '',
SMTPAccount: '',
SMTPFrom: '',
SMTPToken: '',
SMTPSSLEnabled: false,
SMTPForceAuthLogin: false,
WorkerUrl: '',
WorkerValidKey: '',
WorkerAllowHttpImageRequestEnabled: false,
LogConsumeEnabled: false,
'performance_setting.disk_cache_enabled': false,
'performance_setting.disk_cache_threshold_mb': 10,
'performance_setting.disk_cache_max_size_mb': 1024,
'performance_setting.disk_cache_path': '',
'performance_setting.monitor_enabled': false,
'performance_setting.monitor_cpu_threshold': 90,
'performance_setting.monitor_memory_threshold': 90,
'performance_setting.monitor_disk_threshold': 95,
'perf_metrics_setting.enabled': true,
'perf_metrics_setting.flush_interval': 5,
'perf_metrics_setting.bucket_time': 'hour',
'perf_metrics_setting.retention_days': 0,
}
export function OperationsSettings() {
const { t } = useTranslation()
const { data, isLoading } = useSystemOptions()
const { status } = useStatus()
const params = useParams({
from: '/_authenticated/system-settings/maintenance/$section',
from: '/_authenticated/system-settings/operations/$section',
})
const settings = useMemo(
() => getOptionValue(data?.data, DEFAULT_MAINTENANCE_SETTINGS),
() => getOptionValue(data?.data, defaultOperationsSettings),
[data?.data]
)
......@@ -30,14 +70,15 @@ export function MaintenanceSettings() {
)
}
const activeSection = (params?.section ?? MAINTENANCE_DEFAULT_SECTION) as
| 'update-checker'
| 'notice'
const activeSection = (params?.section ?? OPERATIONS_DEFAULT_SECTION) as
| 'behavior'
| 'monitoring'
| 'email'
| 'worker'
| 'logs'
| 'header-navigation'
| 'sidebar-modules'
| 'performance'
const sectionContent = getMaintenanceSectionContent(
| 'update-checker'
const sectionContent = getOperationsSectionContent(
activeSection,
settings,
status?.version as string | undefined,
......
import type { OperationsSettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry'
import { SystemBehaviorSection } from '../general/system-behavior-section'
import { EmailSettingsSection } from '../integrations/email-settings-section'
import { MonitoringSettingsSection } from '../integrations/monitoring-settings-section'
import { WorkerSettingsSection } from '../integrations/worker-settings-section'
import { LogSettingsSection } from '../maintenance/log-settings-section'
import { PerformanceSection } from '../maintenance/performance-section'
import { UpdateCheckerSection } from '../maintenance/update-checker-section'
const OPERATIONS_SECTIONS = [
{
id: 'behavior',
titleKey: 'System Behavior',
descriptionKey: 'Configure system-wide behavior and defaults',
build: (settings: OperationsSettings) => (
<SystemBehaviorSection
defaultValues={{
RetryTimes: settings.RetryTimes,
DefaultCollapseSidebar: settings.DefaultCollapseSidebar,
DemoSiteEnabled: settings.DemoSiteEnabled,
SelfUseModeEnabled: settings.SelfUseModeEnabled,
}}
/>
),
},
{
id: 'monitoring',
titleKey: 'Monitoring & Alerts',
descriptionKey: 'Configure channel monitoring and automation',
build: (settings: OperationsSettings) => (
<MonitoringSettingsSection
defaultValues={{
ChannelDisableThreshold: settings.ChannelDisableThreshold,
QuotaRemindThreshold: settings.QuotaRemindThreshold,
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: 'email',
titleKey: 'SMTP Email',
descriptionKey: 'Configure SMTP email settings',
build: (settings: OperationsSettings) => (
<EmailSettingsSection
defaultValues={{
SMTPServer: settings.SMTPServer,
SMTPPort: settings.SMTPPort,
SMTPAccount: settings.SMTPAccount,
SMTPFrom: settings.SMTPFrom,
SMTPToken: settings.SMTPToken,
SMTPSSLEnabled: settings.SMTPSSLEnabled,
SMTPForceAuthLogin: settings.SMTPForceAuthLogin,
}}
/>
),
},
{
id: 'worker',
titleKey: 'Worker Proxy',
descriptionKey: 'Configure worker service settings',
build: (settings: OperationsSettings) => (
<WorkerSettingsSection
defaultValues={{
WorkerUrl: settings.WorkerUrl,
WorkerValidKey: settings.WorkerValidKey,
WorkerAllowHttpImageRequestEnabled:
settings.WorkerAllowHttpImageRequestEnabled,
}}
/>
),
},
{
id: 'logs',
titleKey: 'Log Maintenance',
descriptionKey: 'Configure log consumption settings',
build: (settings: OperationsSettings) => (
<LogSettingsSection
defaultEnabled={Boolean(settings.LogConsumeEnabled)}
/>
),
},
{
id: 'performance',
titleKey: 'Performance',
descriptionKey: 'Disk cache, system monitoring and performance stats',
build: (settings: OperationsSettings) => (
<PerformanceSection
defaultValues={{
'performance_setting.disk_cache_enabled':
settings['performance_setting.disk_cache_enabled'] ?? false,
'performance_setting.disk_cache_threshold_mb':
settings['performance_setting.disk_cache_threshold_mb'] ?? 10,
'performance_setting.disk_cache_max_size_mb':
settings['performance_setting.disk_cache_max_size_mb'] ?? 1024,
'performance_setting.disk_cache_path':
settings['performance_setting.disk_cache_path'] ?? '',
'performance_setting.monitor_enabled':
settings['performance_setting.monitor_enabled'] ?? false,
'performance_setting.monitor_cpu_threshold':
settings['performance_setting.monitor_cpu_threshold'] ?? 90,
'performance_setting.monitor_memory_threshold':
settings['performance_setting.monitor_memory_threshold'] ?? 90,
'performance_setting.monitor_disk_threshold':
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,
}}
/>
),
},
{
id: 'update-checker',
titleKey: 'System maintenance',
descriptionKey: 'Check for system updates',
build: (
_settings: OperationsSettings,
currentVersion?: string | null,
startTime?: number | null
) => (
<UpdateCheckerSection
currentVersion={currentVersion}
startTime={startTime}
/>
),
},
] as const
export type OperationsSectionId = (typeof OPERATIONS_SECTIONS)[number]['id']
const operationsRegistry = createSectionRegistry<
OperationsSectionId,
OperationsSettings,
[string | null | undefined, number | null | undefined]
>({
sections: OPERATIONS_SECTIONS,
defaultSection: 'behavior',
basePath: '/system-settings/operations',
urlStyle: 'path',
})
export const OPERATIONS_SECTION_IDS = operationsRegistry.sectionIds
export const OPERATIONS_DEFAULT_SECTION = operationsRegistry.defaultSection
export const getOperationsSectionNavItems =
operationsRegistry.getSectionNavItems
export const getOperationsSectionContent =
operationsRegistry.getSectionContent
import { SettingsPage } from '../components/settings-page'
import type { RequestLimitsSettings } from '../types'
import type { SecuritySettings } from '../types'
import {
REQUEST_LIMITS_DEFAULT_SECTION,
getRequestLimitsSectionContent,
SECURITY_DEFAULT_SECTION,
getSecuritySectionContent,
} from './section-registry.tsx'
const defaultRequestLimitsSettings: RequestLimitsSettings = {
const defaultSecuritySettings: SecuritySettings = {
ModelRequestRateLimitEnabled: false,
ModelRequestRateLimitCount: 0,
ModelRequestRateLimitSuccessCount: 1000,
......@@ -24,13 +24,13 @@ const defaultRequestLimitsSettings: RequestLimitsSettings = {
'fetch_setting.apply_ip_filter_for_domain': false,
}
export function RequestLimitsSettings() {
export function SecuritySettings() {
return (
<SettingsPage
routePath='/_authenticated/system-settings/request-limits/$section'
defaultSettings={defaultRequestLimitsSettings}
defaultSection={REQUEST_LIMITS_DEFAULT_SECTION}
getSectionContent={getRequestLimitsSectionContent}
routePath='/_authenticated/system-settings/security/$section'
defaultSettings={defaultSecuritySettings}
defaultSection={SECURITY_DEFAULT_SECTION}
getSectionContent={getSecuritySectionContent}
/>
)
}
import type { RequestLimitsSettings } from '../types'
import type { SecuritySettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry'
import { RateLimitSection } from './rate-limit-section'
import { SensitiveWordsSection } from './sensitive-words-section'
import { SSRFSection } from './ssrf-section'
import { RateLimitSection } from '../request-limits/rate-limit-section'
import { SensitiveWordsSection } from '../request-limits/sensitive-words-section'
import { SSRFSection } from '../request-limits/ssrf-section'
const REQUEST_LIMITS_SECTIONS = [
const SECURITY_SECTIONS = [
{
id: 'rate-limit',
titleKey: 'Rate Limiting',
descriptionKey: 'Configure model request rate limiting',
build: (settings: RequestLimitsSettings) => (
build: (settings: SecuritySettings) => (
<RateLimitSection
defaultValues={{
ModelRequestRateLimitEnabled: settings.ModelRequestRateLimitEnabled,
......@@ -27,7 +27,7 @@ const REQUEST_LIMITS_SECTIONS = [
id: 'sensitive-words',
titleKey: 'Sensitive Words',
descriptionKey: 'Configure sensitive word filtering',
build: (settings: RequestLimitsSettings) => (
build: (settings: SecuritySettings) => (
<SensitiveWordsSection
defaultValues={{
CheckSensitiveEnabled: settings.CheckSensitiveEnabled,
......@@ -41,7 +41,7 @@ const REQUEST_LIMITS_SECTIONS = [
id: 'ssrf',
titleKey: 'SSRF Protection',
descriptionKey: 'Configure SSRF (Server-Side Request Forgery) protection',
build: (settings: RequestLimitsSettings) => (
build: (settings: SecuritySettings) => (
<SSRFSection
defaultValues={{
'fetch_setting.enable_ssrf_protection':
......@@ -64,23 +64,19 @@ const REQUEST_LIMITS_SECTIONS = [
},
] as const
export type RequestLimitsSectionId =
(typeof REQUEST_LIMITS_SECTIONS)[number]['id']
export type SecuritySectionId = (typeof SECURITY_SECTIONS)[number]['id']
const requestLimitsRegistry = createSectionRegistry<
RequestLimitsSectionId,
RequestLimitsSettings
const securityRegistry = createSectionRegistry<
SecuritySectionId,
SecuritySettings
>({
sections: REQUEST_LIMITS_SECTIONS,
sections: SECURITY_SECTIONS,
defaultSection: 'rate-limit',
basePath: '/system-settings/request-limits',
basePath: '/system-settings/security',
urlStyle: 'path',
})
export const REQUEST_LIMITS_SECTION_IDS = requestLimitsRegistry.sectionIds
export const REQUEST_LIMITS_DEFAULT_SECTION =
requestLimitsRegistry.defaultSection
export const getRequestLimitsSectionNavItems =
requestLimitsRegistry.getSectionNavItems
export const getRequestLimitsSectionContent =
requestLimitsRegistry.getSectionContent
export const SECURITY_SECTION_IDS = securityRegistry.sectionIds
export const SECURITY_DEFAULT_SECTION = securityRegistry.defaultSection
export const getSecuritySectionNavItems = securityRegistry.getSectionNavItems
export const getSecuritySectionContent = securityRegistry.getSectionContent
import { SettingsPage } from '../components/settings-page'
import type { SiteSettings } from '../types'
import {
SITE_DEFAULT_SECTION,
getSiteSectionContent,
} from './section-registry.tsx'
const defaultSiteSettings: SiteSettings = {
'theme.frontend': 'default',
Notice: '',
SystemName: 'New API',
Logo: '',
Footer: '',
About: '',
HomePageContent: '',
ServerAddress: '',
'legal.user_agreement': '',
'legal.privacy_policy': '',
HeaderNavModules: '',
SidebarModulesAdmin: '',
}
export function SiteSettings() {
return (
<SettingsPage
routePath='/_authenticated/system-settings/site/$section'
defaultSettings={defaultSiteSettings}
defaultSection={SITE_DEFAULT_SECTION}
getSectionContent={getSiteSectionContent}
/>
)
}
import type { SiteSettings } from '../types'
import { createSectionRegistry } from '../utils/section-registry'
import {
parseHeaderNavModules,
parseSidebarModulesAdmin,
serializeHeaderNavModules,
serializeSidebarModulesAdmin,
} from '../maintenance/config'
import { HeaderNavigationSection } from '../maintenance/header-navigation-section'
import { NoticeSection } from '../maintenance/notice-section'
import { SidebarModulesSection } from '../maintenance/sidebar-modules-section'
import { SystemInfoSection } from '../general/system-info-section'
const SITE_SECTIONS = [
{
id: 'system-info',
titleKey: 'System Information',
descriptionKey: 'Configure basic system information and branding',
build: (settings: SiteSettings) => (
<SystemInfoSection
defaultValues={{
theme: {
frontend: settings['theme.frontend'] as 'default' | 'classic',
},
SystemName: settings.SystemName,
Logo: settings.Logo,
Footer: settings.Footer,
About: settings.About,
HomePageContent: settings.HomePageContent,
ServerAddress: settings.ServerAddress,
legal: {
user_agreement: settings['legal.user_agreement'],
privacy_policy: settings['legal.privacy_policy'],
},
}}
/>
),
},
{
id: 'notice',
titleKey: 'System Notice',
descriptionKey: 'Configure system maintenance notice',
build: (settings: SiteSettings) => (
<NoticeSection defaultValue={settings.Notice ?? ''} />
),
},
{
id: 'header-navigation',
titleKey: 'Header navigation',
descriptionKey: 'Configure header navigation modules',
build: (settings: SiteSettings) => {
const headerNavConfig = parseHeaderNavModules(settings.HeaderNavModules)
const headerNavSerialized = serializeHeaderNavModules(headerNavConfig)
return (
<HeaderNavigationSection
config={headerNavConfig}
initialSerialized={headerNavSerialized}
/>
)
},
},
{
id: 'sidebar-modules',
titleKey: 'Sidebar modules',
descriptionKey: 'Configure sidebar modules for admin',
build: (settings: SiteSettings) => {
const sidebarConfig = parseSidebarModulesAdmin(
settings.SidebarModulesAdmin
)
const sidebarSerialized = serializeSidebarModulesAdmin(sidebarConfig)
return (
<SidebarModulesSection
config={sidebarConfig}
initialSerialized={sidebarSerialized}
/>
)
},
},
] as const
export type SiteSectionId = (typeof SITE_SECTIONS)[number]['id']
const siteRegistry = createSectionRegistry<SiteSectionId, SiteSettings>({
sections: SITE_SECTIONS,
defaultSection: 'system-info',
basePath: '/system-settings/site',
urlStyle: 'path',
})
export const SITE_SECTION_IDS = siteRegistry.sectionIds
export const SITE_DEFAULT_SECTION = siteRegistry.defaultSection
export const getSiteSectionNavItems = siteRegistry.getSectionNavItems
export const getSiteSectionContent = siteRegistry.getSectionContent
......@@ -27,7 +27,7 @@ export type DeleteLogsResponse = {
data?: number
}
export type GeneralSettings = {
export type SiteSettings = {
'theme.frontend': string
Notice: string
SystemName: string
......@@ -38,32 +38,8 @@ export type GeneralSettings = {
ServerAddress: string
'legal.user_agreement': string
'legal.privacy_policy': string
QuotaForNewUser: number
PreConsumedQuota: number
QuotaForInviter: number
QuotaForInvitee: number
TopUpLink: string
'general_setting.docs_link': string
'quota_setting.enable_free_model_pre_consume': boolean
QuotaPerUnit: number
USDExchangeRate: number
'general_setting.quota_display_type': string
'general_setting.custom_currency_symbol': string
'general_setting.custom_currency_exchange_rate': number
RetryTimes: number
DisplayInCurrencyEnabled: boolean
DisplayTokenStatEnabled: boolean
DefaultCollapseSidebar: boolean
DemoSiteEnabled: boolean
SelfUseModeEnabled: boolean
'checkin_setting.enabled': boolean
'checkin_setting.min_quota': number
'checkin_setting.max_quota': number
'channel_affinity_setting.enabled': boolean
'channel_affinity_setting.switch_on_success': boolean
'channel_affinity_setting.max_entries': number
'channel_affinity_setting.default_ttl_seconds': number
'channel_affinity_setting.rules': string
HeaderNavModules: string
SidebarModulesAdmin: string
}
export type AuthSettings = {
......@@ -131,28 +107,87 @@ export type ContentSettings = {
MjActionCheckSuccessEnabled: boolean
}
export type IntegrationSettings = {
SMTPServer: string
SMTPPort: string
SMTPAccount: string
SMTPFrom: string
SMTPToken: string
SMTPSSLEnabled: boolean
SMTPForceAuthLogin: boolean
WorkerUrl: string
WorkerValidKey: string
WorkerAllowHttpImageRequestEnabled: boolean
ChannelDisableThreshold: 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
export type ModelSettings = {
'global.pass_through_request_enabled': boolean
'global.thinking_model_blacklist': string
'global.chat_completions_to_responses_policy': string
'general_setting.ping_interval_enabled': boolean
'general_setting.ping_interval_seconds': number
'gemini.safety_settings': string
'gemini.version_settings': string
'gemini.supported_imagine_models': string
'gemini.thinking_adapter_enabled': boolean
'gemini.thinking_adapter_budget_tokens_percentage': number
'gemini.function_call_thought_signature_enabled': boolean
'gemini.remove_function_response_id_enabled': boolean
'claude.model_headers_settings': string
'claude.default_max_tokens': string
'claude.thinking_adapter_enabled': boolean
'claude.thinking_adapter_budget_tokens_percentage': number
'grok.violation_deduction_enabled': boolean
'grok.violation_deduction_amount': number
ModelPrice: string
ModelRatio: string
CacheRatio: string
CreateCacheRatio: string
CompletionRatio: string
ImageRatio: string
AudioRatio: string
AudioCompletionRatio: string
ExposeRatioEnabled: boolean
'billing_setting.billing_mode': string
'billing_setting.billing_expr': string
'tool_price_setting.prices': string
TopupGroupRatio: string
GroupRatio: string
UserUsableGroups: string
GroupGroupRatio: string
AutoGroups: string
DefaultUseAutoGroup: boolean
'group_ratio_setting.group_special_usable_group': string
'channel_affinity_setting.enabled': boolean
'channel_affinity_setting.switch_on_success': boolean
'channel_affinity_setting.max_entries': number
'channel_affinity_setting.default_ttl_seconds': number
'channel_affinity_setting.rules': string
'model_deployment.ionet.api_key': string
'model_deployment.ionet.enabled': boolean
}
export type BillingSettings = {
QuotaForNewUser: number
PreConsumedQuota: number
QuotaForInviter: number
QuotaForInvitee: number
TopUpLink: string
'general_setting.docs_link': string
'quota_setting.enable_free_model_pre_consume': boolean
QuotaPerUnit: number
USDExchangeRate: number
'general_setting.quota_display_type': string
'general_setting.custom_currency_symbol': string
'general_setting.custom_currency_exchange_rate': number
DisplayInCurrencyEnabled: boolean
DisplayTokenStatEnabled: boolean
ModelPrice: string
ModelRatio: string
CacheRatio: string
CreateCacheRatio: string
CompletionRatio: string
ImageRatio: string
AudioRatio: string
AudioCompletionRatio: string
ExposeRatioEnabled: boolean
'billing_setting.billing_mode': string
'billing_setting.billing_expr': string
'tool_price_setting.prices': string
TopupGroupRatio: string
GroupRatio: string
UserUsableGroups: string
GroupGroupRatio: string
AutoGroups: string
DefaultUseAutoGroup: boolean
'group_ratio_setting.group_special_usable_group': string
PayAddress: string
EpayId: string
EpayKey: string
......@@ -199,53 +234,36 @@ export type IntegrationSettings = {
WaffoPancakeCurrency: string
WaffoPancakeUnitPrice: number
WaffoPancakeMinTopUp: number
'checkin_setting.enabled': boolean
'checkin_setting.min_quota': number
'checkin_setting.max_quota': number
}
export type ModelSettings = {
'global.pass_through_request_enabled': boolean
'global.thinking_model_blacklist': string
'global.chat_completions_to_responses_policy': string
'general_setting.ping_interval_enabled': boolean
'general_setting.ping_interval_seconds': number
'gemini.safety_settings': string
'gemini.version_settings': string
'gemini.supported_imagine_models': string
'gemini.thinking_adapter_enabled': boolean
'gemini.thinking_adapter_budget_tokens_percentage': number
'gemini.function_call_thought_signature_enabled': boolean
'gemini.remove_function_response_id_enabled': boolean
'claude.model_headers_settings': string
'claude.default_max_tokens': string
'claude.thinking_adapter_enabled': boolean
'claude.thinking_adapter_budget_tokens_percentage': number
'grok.violation_deduction_enabled': boolean
'grok.violation_deduction_amount': number
ModelPrice: string
ModelRatio: string
CacheRatio: string
CreateCacheRatio: string
CompletionRatio: string
ImageRatio: string
AudioRatio: string
AudioCompletionRatio: string
ExposeRatioEnabled: boolean
'billing_setting.billing_mode': string
'billing_setting.billing_expr': string
'tool_price_setting.prices': string
TopupGroupRatio: string
GroupRatio: string
UserUsableGroups: string
GroupGroupRatio: string
AutoGroups: string
DefaultUseAutoGroup: boolean
'group_ratio_setting.group_special_usable_group': string
}
export type MaintenanceSettings = {
Notice: string
export type OperationsSettings = {
RetryTimes: number
DefaultCollapseSidebar: boolean
DemoSiteEnabled: boolean
SelfUseModeEnabled: boolean
ChannelDisableThreshold: 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
SMTPPort: string
SMTPAccount: string
SMTPFrom: string
SMTPToken: string
SMTPSSLEnabled: boolean
SMTPForceAuthLogin: boolean
WorkerUrl: string
WorkerValidKey: string
WorkerAllowHttpImageRequestEnabled: boolean
LogConsumeEnabled: boolean
HeaderNavModules: string
SidebarModulesAdmin: string
'performance_setting.disk_cache_enabled': boolean
'performance_setting.disk_cache_threshold_mb': number
'performance_setting.disk_cache_max_size_mb': number
......@@ -260,7 +278,7 @@ export type MaintenanceSettings = {
'perf_metrics_setting.retention_days': number
}
export type RequestLimitsSettings = {
export type SecuritySettings = {
ModelRequestRateLimitEnabled: boolean
ModelRequestRateLimitCount: number
ModelRequestRateLimitSuccessCount: number
......
......@@ -29,7 +29,7 @@ export type SettingsRouteConfigOptions<
defaultSection: TSectionId
/** Settings component to render */
component: TComponent
/** Route path for redirect (e.g., '/system-settings/general') */
/** Route path for redirect (e.g., '/system-settings/site') */
routePath: string
/** Whether to redirect to default section if no section is provided (default: false) */
redirectToDefault?: boolean
......@@ -44,12 +44,12 @@ export type SettingsRouteConfigOptions<
*
* @example
* ```tsx
* export const Route = createFileRoute('/_authenticated/system-settings/general')(
* export const Route = createFileRoute('/_authenticated/system-settings/site')(
* createSettingsRouteConfig({
* sectionIds: GENERAL_SECTION_IDS,
* defaultSection: GENERAL_DEFAULT_SECTION,
* component: GeneralSettings,
* routePath: '/system-settings/general',
* sectionIds: SITE_SECTION_IDS,
* defaultSection: SITE_DEFAULT_SECTION,
* component: SiteSettings,
* routePath: '/system-settings/site',
* redirectToDefault: true,
* })
* )
......
......@@ -96,7 +96,7 @@ const URL_TO_CONFIG_MAP: Record<string, { section: string; module: string }> = {
'/redemption-codes': { section: 'admin', module: 'redemption' },
'/subscriptions': { section: 'admin', module: 'subscription' },
'/system-settings': { section: 'admin', module: 'setting' },
'/system-settings/general': { section: 'admin', module: 'setting' },
'/system-settings/site': { section: 'admin', module: 'setting' },
}
/**
......
......@@ -129,7 +129,7 @@ export function useSidebarData(): SidebarData {
},
{
title: t('System Settings'),
url: '/system-settings/general',
url: '/system-settings/site',
activeUrls: ['/system-settings'],
icon: Settings,
},
......
......@@ -11,25 +11,25 @@
"file": "fr.json",
"missingCount": 0,
"extrasCount": 0,
"untranslatedCount": 0
"untranslatedCount": 1
},
"ja": {
"file": "ja.json",
"missingCount": 0,
"extrasCount": 0,
"untranslatedCount": 88
"untranslatedCount": 90
},
"ru": {
"file": "ru.json",
"missingCount": 0,
"extrasCount": 0,
"untranslatedCount": 92
"untranslatedCount": 105
},
"vi": {
"file": "vi.json",
"missingCount": 0,
"extrasCount": 0,
"untranslatedCount": 0
"untranslatedCount": 3
},
"zh": {
"file": "zh.json",
......
{
"apps using the most tokens through new-api": "Apps using the most tokens through new-api",
"apps using the most tokens through new-api.": "Apps using the most tokens through new-api."
"Each tier supports up to 2 conditions. The last tier without conditions is the fallback.": "Each tier supports up to 2 conditions. The last tier without conditions is the fallback."
}
......@@ -77,6 +77,8 @@
"Submodel": "Submodel",
"SunoAPI": "SunoAPI",
"Telegram": "Telegram",
"Token prices": "Token prices",
"Each tier supports up to 2 conditions. The last tier without conditions is the fallback.": "Each tier supports up to 2 conditions. The last tier without conditions is the fallback.",
"TTFT P50": "TTFT P50",
"TTFT P95": "TTFT P95",
"TTFT P99": "TTFT P99",
......
......@@ -81,6 +81,8 @@
"SunoAPI": "SunoAPI",
"Telegram": "Telegram",
"Tencent": "Tencent",
"Token prices": "Token prices",
"Each tier supports up to 2 conditions. The last tier without conditions is the fallback.": "Each tier supports up to 2 conditions. The last tier without conditions is the fallback.",
"TTFT P50": "TTFT P50",
"TTFT P95": "TTFT P95",
"TTFT P99": "TTFT P99",
......@@ -90,5 +92,16 @@
"whsec_xxx": "whsec_xxx",
"Xinference": "Xinference",
"Xunfei": "Xunfei",
"Zhipu V4": "Zhipu V4"
"Zhipu V4": "Zhipu V4",
"Cache pricing": "Cache pricing",
"Core pricing": "Core pricing",
"All conditions must match before this tier is used.": "All conditions must match before this tier is used.",
"Base input and output token prices for this tier.": "Base input and output token prices for this tier.",
"Fallback tier": "Fallback tier",
"Media pricing": "Media pricing",
"No separate media pricing configured.": "No separate media pricing configured.",
"Separate image/audio prices are enabled.": "Separate image/audio prices are enabled.",
"Set separate prices for cache reads and writes.": "Set separate prices for cache reads and writes.",
"This tier catches any request that did not match earlier tiers.": "This tier catches any request that did not match earlier tiers.",
"Tier conditions": "Tier conditions"
}
{
"apps using the most tokens through new-api": "Apps using the most tokens through new-api",
"apps using the most tokens through new-api.": "Apps using the most tokens through new-api."
"Each tier supports up to 2 conditions. The last tier without conditions is the fallback.": "Each tier supports up to 2 conditions. The last tier without conditions is the fallback.",
"Base input and output token prices for this tier.": "Base input and output token prices for this tier.",
"Set separate prices for cache reads and writes.": "Set separate prices for cache reads and writes."
}
import { createFileRoute, redirect } from '@tanstack/react-router'
import { GeneralSettings } from '@/features/system-settings/general'
import { BillingSettings } from '@/features/system-settings/billing'
import {
GENERAL_DEFAULT_SECTION,
GENERAL_SECTION_IDS,
} from '@/features/system-settings/general/section-registry.tsx'
BILLING_DEFAULT_SECTION,
BILLING_SECTION_IDS,
} from '@/features/system-settings/billing/section-registry.tsx'
export const Route = createFileRoute(
'/_authenticated/system-settings/general/$section'
'/_authenticated/system-settings/billing/$section'
)({
beforeLoad: ({ params }) => {
const validSections = GENERAL_SECTION_IDS as unknown as string[]
const validSections = BILLING_SECTION_IDS as unknown as string[]
if (!validSections.includes(params.section)) {
throw redirect({
to: '/system-settings/general/$section',
params: { section: GENERAL_DEFAULT_SECTION },
to: '/system-settings/billing/$section',
params: { section: BILLING_DEFAULT_SECTION },
})
}
},
component: GeneralSettings,
component: BillingSettings,
})
import { createFileRoute, redirect } from '@tanstack/react-router'
import { GENERAL_DEFAULT_SECTION } from '@/features/system-settings/general/section-registry.tsx'
import { BILLING_DEFAULT_SECTION } from '@/features/system-settings/billing/section-registry.tsx'
export const Route = createFileRoute(
'/_authenticated/system-settings/general/'
'/_authenticated/system-settings/billing/'
)({
beforeLoad: () => {
throw redirect({
to: '/system-settings/general/$section',
params: { section: GENERAL_DEFAULT_SECTION },
to: '/system-settings/billing/$section',
params: { section: BILLING_DEFAULT_SECTION },
})
},
})
......@@ -3,7 +3,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated/system-settings/')({
beforeLoad: () => {
throw redirect({
to: '/system-settings/general',
to: '/system-settings/site',
})
},
})
import { createFileRoute, redirect } from '@tanstack/react-router'
import { IntegrationSettings } from '@/features/system-settings/integrations'
import { OperationsSettings } from '@/features/system-settings/operations'
import {
INTEGRATIONS_DEFAULT_SECTION,
INTEGRATIONS_SECTION_IDS,
} from '@/features/system-settings/integrations/section-registry.tsx'
OPERATIONS_DEFAULT_SECTION,
OPERATIONS_SECTION_IDS,
} from '@/features/system-settings/operations/section-registry.tsx'
export const Route = createFileRoute(
'/_authenticated/system-settings/integrations/$section'
'/_authenticated/system-settings/operations/$section'
)({
beforeLoad: ({ params }) => {
const validSections = INTEGRATIONS_SECTION_IDS as unknown as string[]
const validSections = OPERATIONS_SECTION_IDS as unknown as string[]
if (!validSections.includes(params.section)) {
throw redirect({
to: '/system-settings/integrations/$section',
params: { section: INTEGRATIONS_DEFAULT_SECTION },
to: '/system-settings/operations/$section',
params: { section: OPERATIONS_DEFAULT_SECTION },
})
}
},
component: IntegrationSettings,
component: OperationsSettings,
})
import { createFileRoute, redirect } from '@tanstack/react-router'
import { INTEGRATIONS_DEFAULT_SECTION } from '@/features/system-settings/integrations/section-registry.tsx'
import { OPERATIONS_DEFAULT_SECTION } from '@/features/system-settings/operations/section-registry.tsx'
export const Route = createFileRoute(
'/_authenticated/system-settings/integrations/'
'/_authenticated/system-settings/operations/'
)({
beforeLoad: () => {
throw redirect({
to: '/system-settings/integrations/$section',
params: { section: INTEGRATIONS_DEFAULT_SECTION },
to: '/system-settings/operations/$section',
params: { section: OPERATIONS_DEFAULT_SECTION },
})
},
})
import { createFileRoute, redirect } from '@tanstack/react-router'
import { MaintenanceSettings } from '@/features/system-settings/maintenance'
import { SecuritySettings } from '@/features/system-settings/security'
import {
MAINTENANCE_DEFAULT_SECTION,
MAINTENANCE_SECTION_IDS,
} from '@/features/system-settings/maintenance/section-registry.tsx'
SECURITY_DEFAULT_SECTION,
SECURITY_SECTION_IDS,
} from '@/features/system-settings/security/section-registry.tsx'
export const Route = createFileRoute(
'/_authenticated/system-settings/maintenance/$section'
'/_authenticated/system-settings/security/$section'
)({
beforeLoad: ({ params }) => {
const validSections = MAINTENANCE_SECTION_IDS as unknown as string[]
const validSections = SECURITY_SECTION_IDS as unknown as string[]
if (!validSections.includes(params.section)) {
throw redirect({
to: '/system-settings/maintenance/$section',
params: { section: MAINTENANCE_DEFAULT_SECTION },
to: '/system-settings/security/$section',
params: { section: SECURITY_DEFAULT_SECTION },
})
}
},
component: MaintenanceSettings,
component: SecuritySettings,
})
import { createFileRoute, redirect } from '@tanstack/react-router'
import { MAINTENANCE_DEFAULT_SECTION } from '@/features/system-settings/maintenance/section-registry.tsx'
import { SECURITY_DEFAULT_SECTION } from '@/features/system-settings/security/section-registry.tsx'
export const Route = createFileRoute(
'/_authenticated/system-settings/maintenance/'
'/_authenticated/system-settings/security/'
)({
beforeLoad: () => {
throw redirect({
to: '/system-settings/maintenance/$section',
params: { section: MAINTENANCE_DEFAULT_SECTION },
to: '/system-settings/security/$section',
params: { section: SECURITY_DEFAULT_SECTION },
})
},
})
import { createFileRoute, redirect } from '@tanstack/react-router'
import { RequestLimitsSettings } from '@/features/system-settings/request-limits'
import { SiteSettings } from '@/features/system-settings/site'
import {
REQUEST_LIMITS_DEFAULT_SECTION,
REQUEST_LIMITS_SECTION_IDS,
} from '@/features/system-settings/request-limits/section-registry.tsx'
SITE_DEFAULT_SECTION,
SITE_SECTION_IDS,
} from '@/features/system-settings/site/section-registry.tsx'
export const Route = createFileRoute(
'/_authenticated/system-settings/request-limits/$section'
'/_authenticated/system-settings/site/$section'
)({
beforeLoad: ({ params }) => {
const validSections = REQUEST_LIMITS_SECTION_IDS as unknown as string[]
const validSections = SITE_SECTION_IDS as unknown as string[]
if (!validSections.includes(params.section)) {
throw redirect({
to: '/system-settings/request-limits/$section',
params: { section: REQUEST_LIMITS_DEFAULT_SECTION },
to: '/system-settings/site/$section',
params: { section: SITE_DEFAULT_SECTION },
})
}
},
component: RequestLimitsSettings,
component: SiteSettings,
})
import { createFileRoute, redirect } from '@tanstack/react-router'
import { REQUEST_LIMITS_DEFAULT_SECTION } from '@/features/system-settings/request-limits/section-registry.tsx'
import { SITE_DEFAULT_SECTION } from '@/features/system-settings/site/section-registry.tsx'
export const Route = createFileRoute(
'/_authenticated/system-settings/request-limits/'
)({
export const Route = createFileRoute('/_authenticated/system-settings/site/')({
beforeLoad: () => {
throw redirect({
to: '/system-settings/request-limits/$section',
params: { section: REQUEST_LIMITS_DEFAULT_SECTION },
to: '/system-settings/site/$section',
params: { section: SITE_DEFAULT_SECTION },
})
},
})
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