Просмотр исходного кода

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.
CaIon 3 дней назад
Родитель
Сommit
0f9f094a48
62 измененных файлов с 3650 добавлено и 2338 удалено
  1. 1 1
      web/default/src/components/layout/components/workspace-switcher.tsx
  2. 23 23
      web/default/src/components/layout/config/system-settings.config.ts
  3. 2 2
      web/default/src/components/profile-dropdown.tsx
  4. 20 1
      web/default/src/features/errors/general-error.tsx
  5. 4 1
      web/default/src/features/models/components/deployment-access-guard.tsx
  6. 7 0
      web/default/src/features/models/components/drawers/model-mutate-drawer.tsx
  7. 102 0
      web/default/src/features/system-settings/billing/index.tsx
  8. 202 0
      web/default/src/features/system-settings/billing/section-registry.tsx
  9. 0 89
      web/default/src/features/system-settings/general/index.tsx
  10. 0 148
      web/default/src/features/system-settings/general/section-registry.tsx
  11. 0 28
      web/default/src/features/system-settings/general/system-info-section.tsx
  12. 0 88
      web/default/src/features/system-settings/integrations/index.tsx
  13. 0 160
      web/default/src/features/system-settings/integrations/section-registry.tsx
  14. 0 21
      web/default/src/features/system-settings/maintenance/config.ts
  15. 0 54
      web/default/src/features/system-settings/maintenance/index.tsx
  16. 0 137
      web/default/src/features/system-settings/maintenance/section-registry.tsx
  17. 7 0
      web/default/src/features/system-settings/models/index.tsx
  18. 1032 0
      web/default/src/features/system-settings/models/model-pricing-sheet.tsx
  19. 0 656
      web/default/src/features/system-settings/models/model-ratio-dialog.tsx
  20. 476 234
      web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
  21. 77 52
      web/default/src/features/system-settings/models/ratio-settings-card.tsx
  22. 29 27
      web/default/src/features/system-settings/models/section-registry.tsx
  23. 245 149
      web/default/src/features/system-settings/models/tiered-pricing-editor.tsx
  24. 95 0
      web/default/src/features/system-settings/operations/index.tsx
  25. 163 0
      web/default/src/features/system-settings/operations/section-registry.tsx
  26. 9 9
      web/default/src/features/system-settings/security/index.tsx
  27. 18 22
      web/default/src/features/system-settings/security/section-registry.tsx
  28. 32 0
      web/default/src/features/system-settings/site/index.tsx
  29. 93 0
      web/default/src/features/system-settings/site/section-registry.tsx
  30. 110 92
      web/default/src/features/system-settings/types.ts
  31. 6 6
      web/default/src/features/system-settings/utils/route-config.ts
  32. 1 1
      web/default/src/hooks/use-sidebar-config.ts
  33. 1 1
      web/default/src/hooks/use-sidebar-data.ts
  34. 4 4
      web/default/src/i18n/locales/_reports/_sync-report.json
  35. 1 2
      web/default/src/i18n/locales/_reports/fr.untranslated.json
  36. 2 0
      web/default/src/i18n/locales/_reports/ja.untranslated.json
  37. 14 1
      web/default/src/i18n/locales/_reports/ru.untranslated.json
  38. 3 2
      web/default/src/i18n/locales/_reports/vi.untranslated.json
  39. 93 2
      web/default/src/i18n/locales/en.json
  40. 93 2
      web/default/src/i18n/locales/fr.json
  41. 93 2
      web/default/src/i18n/locales/ja.json
  42. 94 3
      web/default/src/i18n/locales/ru.json
  43. 94 3
      web/default/src/i18n/locales/vi.json
  44. 93 2
      web/default/src/i18n/locales/zh.json
  45. 176 176
      web/default/src/routeTree.gen.ts
  46. 21 0
      web/default/src/routes/_authenticated/system-settings/billing/$section.tsx
  47. 13 0
      web/default/src/routes/_authenticated/system-settings/billing/index.tsx
  48. 0 21
      web/default/src/routes/_authenticated/system-settings/general/$section.tsx
  49. 0 13
      web/default/src/routes/_authenticated/system-settings/general/index.tsx
  50. 1 1
      web/default/src/routes/_authenticated/system-settings/index.tsx
  51. 0 21
      web/default/src/routes/_authenticated/system-settings/integrations/$section.tsx
  52. 0 13
      web/default/src/routes/_authenticated/system-settings/integrations/index.tsx
  53. 0 21
      web/default/src/routes/_authenticated/system-settings/maintenance/$section.tsx
  54. 0 13
      web/default/src/routes/_authenticated/system-settings/maintenance/index.tsx
  55. 21 0
      web/default/src/routes/_authenticated/system-settings/operations/$section.tsx
  56. 13 0
      web/default/src/routes/_authenticated/system-settings/operations/index.tsx
  57. 0 21
      web/default/src/routes/_authenticated/system-settings/request-limits/$section.tsx
  58. 0 13
      web/default/src/routes/_authenticated/system-settings/request-limits/index.tsx
  59. 21 0
      web/default/src/routes/_authenticated/system-settings/security/$section.tsx
  60. 13 0
      web/default/src/routes/_authenticated/system-settings/security/index.tsx
  61. 21 0
      web/default/src/routes/_authenticated/system-settings/site/$section.tsx
  62. 11 0
      web/default/src/routes/_authenticated/system-settings/site/index.tsx

+ 1 - 1
web/default/src/components/layout/components/workspace-switcher.tsx

@@ -119,7 +119,7 @@ export function WorkspaceSwitcher({
     // Only navigate, let useEffect synchronize workspace state based on new pathname
     // Only navigate, let useEffect synchronize workspace state based on new pathname
     // This avoids race conditions and context loss issues
     // This avoids race conditions and context loss issues
     if (workspace.id === WORKSPACE_IDS.SYSTEM_SETTINGS) {
     if (workspace.id === WORKSPACE_IDS.SYSTEM_SETTINGS) {
-      navigate({ to: '/system-settings/general' })
+      navigate({ to: '/system-settings/site' })
     } else {
     } else {
       navigate({ to: '/dashboard' })
       navigate({ to: '/dashboard' })
     }
     }

+ 23 - 23
web/default/src/components/layout/config/system-settings.config.ts

@@ -1,20 +1,20 @@
 import { type TFunction } from 'i18next'
 import { type TFunction } from 'i18next'
 import {
 import {
+  Box,
+  CreditCard,
+  Layout,
   Settings,
   Settings,
   Shield,
   Shield,
   ShieldAlert,
   ShieldAlert,
-  Layout,
-  Plug,
-  Box,
   Wrench,
   Wrench,
 } from 'lucide-react'
 } from 'lucide-react'
 import { getAuthSectionNavItems } from '@/features/system-settings/auth/section-registry.tsx'
 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 { 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 { 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'
 import { type NavGroup } from '../types'
 
 
 /**
 /**
@@ -30,9 +30,9 @@ export function getSystemSettingsNavGroups(t: TFunction): NavGroup[] {
       title: t('System Administration'),
       title: t('System Administration'),
       items: [
       items: [
         {
         {
-          title: t('General'),
+          title: t('Site & Branding'),
           icon: Settings,
           icon: Settings,
-          items: getGeneralSectionNavItems(t),
+          items: getSiteSectionNavItems(t),
         },
         },
         {
         {
           title: t('Authentication'),
           title: t('Authentication'),
@@ -40,29 +40,29 @@ export function getSystemSettingsNavGroups(t: TFunction): NavGroup[] {
           items: getAuthSectionNavItems(t),
           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,
           icon: Wrench,
-          items: getMaintenanceSectionNavItems(t),
+          items: getOperationsSectionNavItems(t),
         },
         },
       ],
       ],
     },
     },

+ 2 - 2
web/default/src/components/profile-dropdown.tsx

@@ -100,8 +100,8 @@ export function ProfileDropdown() {
             <DropdownMenuItem
             <DropdownMenuItem
               onClick={() =>
               onClick={() =>
                 navigate({
                 navigate({
-                  to: '/system-settings/general',
-                  search: { section: 'system-info' },
+                  to: '/system-settings/site/$section',
+                  params: { section: 'system-info' },
                 })
                 })
               }
               }
             >
             >

+ 20 - 1
web/default/src/features/errors/general-error.tsx

@@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next'
 import { cn } from '@/lib/utils'
 import { cn } from '@/lib/utils'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 
 
+const FEEDBACK_URL = 'https://github.com/QuantumNous/new-api/issues'
+
 type GeneralErrorProps = React.HTMLAttributes<HTMLDivElement> & {
 type GeneralErrorProps = React.HTMLAttributes<HTMLDivElement> & {
   minimal?: boolean
   minimal?: boolean
 }
 }
@@ -28,10 +30,27 @@ export function GeneralError({
           {t('Please try again later.')}
           {t('Please try again later.')}
         </p>
         </p>
         {!minimal && (
         {!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)}>
             <Button variant='outline' onClick={() => history.go(-1)}>
               {t('Go Back')}
               {t('Go Back')}
             </Button>
             </Button>
+            <Button
+              variant='outline'
+              render={
+                <a
+                  href={FEEDBACK_URL}
+                  target='_blank'
+                  rel='noopener noreferrer'
+                />
+              }
+            >
+              {t('Report an issue')}
+            </Button>
             <Button onClick={() => navigate({ to: '/' })}>
             <Button onClick={() => navigate({ to: '/' })}>
               {t('Back to Home')}
               {t('Back to Home')}
             </Button>
             </Button>

+ 4 - 1
web/default/src/features/models/components/deployment-access-guard.tsx

@@ -87,7 +87,10 @@ export function DeploymentAccessGuard({
   const navigate = useNavigate()
   const navigate = useNavigate()
 
 
   const handleGoToSettings = () => {
   const handleGoToSettings = () => {
-    navigate({ to: '/system-settings/integrations' })
+    navigate({
+      to: '/system-settings/models/$section',
+      params: { section: 'model-deployment' },
+    })
   }
   }
 
 
   // Combined loading state with step indicator
   // Combined loading state with step indicator

+ 7 - 0
web/default/src/features/models/components/drawers/model-mutate-drawer.tsx

@@ -168,6 +168,13 @@ 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,
+      '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)
     return getOptionValue(systemOptionsData.data, defaultModelSettings)
   }, [systemOptionsData])
   }, [systemOptionsData])

+ 102 - 0
web/default/src/features/system-settings/billing/index.tsx

@@ -0,0 +1,102 @@
+import { SettingsPage } from '../components/settings-page'
+import type { BillingSettings } from '../types'
+import {
+  BILLING_DEFAULT_SECTION,
+  getBillingSectionContent,
+} from './section-registry.tsx'
+
+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: '',
+  Price: 7.3,
+  MinTopUp: 1,
+  CustomCallbackAddress: '',
+  PayMethods: '',
+  'payment_setting.amount_options': '',
+  'payment_setting.amount_discount': '',
+  StripeApiSecret: '',
+  StripeWebhookSecret: '',
+  StripePriceId: '',
+  StripeUnitPrice: 8.0,
+  StripeMinTopUp: 1,
+  StripePromotionCodesEnabled: false,
+  CreemApiKey: '',
+  CreemWebhookSecret: '',
+  CreemTestMode: false,
+  CreemProducts: '[]',
+  WaffoEnabled: false,
+  WaffoApiKey: '',
+  WaffoPrivateKey: '',
+  WaffoPublicCert: '',
+  WaffoSandboxPublicCert: '',
+  WaffoSandboxApiKey: '',
+  WaffoSandboxPrivateKey: '',
+  WaffoSandbox: false,
+  WaffoMerchantId: '',
+  WaffoCurrency: 'USD',
+  WaffoUnitPrice: 1,
+  WaffoMinTopUp: 1,
+  WaffoNotifyUrl: '',
+  WaffoReturnUrl: '',
+  WaffoPayMethods: '[]',
+  WaffoPancakeEnabled: false,
+  WaffoPancakeSandbox: false,
+  WaffoPancakeMerchantID: '',
+  WaffoPancakePrivateKey: '',
+  WaffoPancakeWebhookPublicKey: '',
+  WaffoPancakeWebhookTestKey: '',
+  WaffoPancakeStoreID: '',
+  WaffoPancakeProductID: '',
+  WaffoPancakeReturnURL: '',
+  WaffoPancakeCurrency: 'USD',
+  WaffoPancakeUnitPrice: 1,
+  WaffoPancakeMinTopUp: 1,
+  'checkin_setting.enabled': false,
+  'checkin_setting.min_quota': 1000,
+  'checkin_setting.max_quota': 10000,
+}
+
+export function BillingSettings() {
+  return (
+    <SettingsPage
+      routePath='/_authenticated/system-settings/billing/$section'
+      defaultSettings={defaultBillingSettings}
+      defaultSection={BILLING_DEFAULT_SECTION}
+      getSectionContent={getBillingSectionContent}
+    />
+  )
+}

+ 202 - 0
web/default/src/features/system-settings/billing/section-registry.tsx

@@ -0,0 +1,202 @@
+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

+ 0 - 89
web/default/src/features/system-settings/general/index.tsx

@@ -1,89 +0,0 @@
-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>
-  )
-}

+ 0 - 148
web/default/src/features/system-settings/general/section-registry.tsx

@@ -1,148 +0,0 @@
-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

+ 0 - 28
web/default/src/features/system-settings/general/system-info-section.tsx

@@ -32,7 +32,6 @@ const _systemInfoSchema = z.object({
   theme: z.object({
   theme: z.object({
     frontend: z.enum(['default', 'classic']),
     frontend: z.enum(['default', 'classic']),
   }),
   }),
-  Notice: z.string().optional(),
   SystemName: z.string().min(1),
   SystemName: z.string().min(1),
   ServerAddress: z.string().optional(),
   ServerAddress: z.string().optional(),
   Logo: z.string().url().optional().or(z.literal('')),
   Logo: z.string().url().optional().or(z.literal('')),
@@ -65,7 +64,6 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) {
       frontend:
       frontend:
         defaultValues.theme?.frontend === 'classic' ? 'classic' : 'default',
         defaultValues.theme?.frontend === 'classic' ? 'classic' : 'default',
     },
     },
-    Notice: normalizeValue(defaultValues.Notice),
     SystemName: normalizeValue(defaultValues.SystemName),
     SystemName: normalizeValue(defaultValues.SystemName),
     ServerAddress: normalizeValue(defaultValues.ServerAddress),
     ServerAddress: normalizeValue(defaultValues.ServerAddress),
     Logo: normalizeValue(defaultValues.Logo),
     Logo: normalizeValue(defaultValues.Logo),
@@ -82,7 +80,6 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) {
     theme: z.object({
     theme: z.object({
       frontend: z.enum(['default', 'classic']),
       frontend: z.enum(['default', 'classic']),
     }),
     }),
-    Notice: z.string().optional(),
     SystemName: z.string().min(1, {
     SystemName: z.string().min(1, {
       error: () => t('System name is required'),
       error: () => t('System name is required'),
     }),
     }),
@@ -161,31 +158,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
             <FormField
               control={form.control}
               control={form.control}
               name='SystemName'
               name='SystemName'

+ 0 - 88
web/default/src/features/system-settings/integrations/index.tsx

@@ -1,88 +0,0 @@
-import { SettingsPage } from '../components/settings-page'
-import type { IntegrationSettings as IntegrationSettingsType } from '../types'
-import {
-  INTEGRATIONS_DEFAULT_SECTION,
-  getIntegrationsSectionContent,
-} 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,
-  PayAddress: '',
-  EpayId: '',
-  EpayKey: '',
-  Price: 7.3,
-  MinTopUp: 1,
-  CustomCallbackAddress: '',
-  PayMethods: '',
-  'payment_setting.amount_options': '',
-  'payment_setting.amount_discount': '',
-  StripeApiSecret: '',
-  StripeWebhookSecret: '',
-  StripePriceId: '',
-  StripeUnitPrice: 8.0,
-  StripeMinTopUp: 1,
-  StripePromotionCodesEnabled: false,
-  CreemApiKey: '',
-  CreemWebhookSecret: '',
-  CreemTestMode: false,
-  CreemProducts: '[]',
-  WaffoEnabled: false,
-  WaffoApiKey: '',
-  WaffoPrivateKey: '',
-  WaffoPublicCert: '',
-  WaffoSandboxPublicCert: '',
-  WaffoSandboxApiKey: '',
-  WaffoSandboxPrivateKey: '',
-  WaffoSandbox: false,
-  WaffoMerchantId: '',
-  WaffoCurrency: 'USD',
-  WaffoUnitPrice: 1,
-  WaffoMinTopUp: 1,
-  WaffoNotifyUrl: '',
-  WaffoReturnUrl: '',
-  WaffoPayMethods: '[]',
-  WaffoPancakeEnabled: false,
-  WaffoPancakeSandbox: false,
-  WaffoPancakeMerchantID: '',
-  WaffoPancakePrivateKey: '',
-  WaffoPancakeWebhookPublicKey: '',
-  WaffoPancakeWebhookTestKey: '',
-  WaffoPancakeStoreID: '',
-  WaffoPancakeProductID: '',
-  WaffoPancakeReturnURL: '',
-  WaffoPancakeCurrency: 'USD',
-  WaffoPancakeUnitPrice: 1,
-  WaffoPancakeMinTopUp: 1,
-}
-
-export function IntegrationSettings() {
-  return (
-    <SettingsPage
-      routePath='/_authenticated/system-settings/integrations/$section'
-      defaultSettings={defaultIntegrationSettings}
-      defaultSection={INTEGRATIONS_DEFAULT_SECTION}
-      getSectionContent={getIntegrationsSectionContent}
-    />
-  )
-}

+ 0 - 160
web/default/src/features/system-settings/integrations/section-registry.tsx

@@ -1,160 +0,0 @@
-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

+ 0 - 21
web/default/src/features/system-settings/maintenance/config.ts

@@ -1,5 +1,3 @@
-import type { MaintenanceSettings } from '../types'
-
 export type HeaderNavPricingConfig = {
 export type HeaderNavPricingConfig = {
   enabled: boolean
   enabled: boolean
   requireAuth: 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 => {
 const toBoolean = (value: unknown, fallback: boolean): boolean => {
   if (typeof value === 'boolean') return value
   if (typeof value === 'boolean') return value
   if (typeof value === 'number') return value === 1
   if (typeof value === 'number') return value === 1

+ 0 - 54
web/default/src/features/system-settings/maintenance/index.tsx

@@ -1,54 +0,0 @@
-import { useMemo } from 'react'
-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 {
-  MAINTENANCE_DEFAULT_SECTION,
-  getMaintenanceSectionContent,
-} from './section-registry.tsx'
-
-export function MaintenanceSettings() {
-  const { t } = useTranslation()
-  const { data, isLoading } = useSystemOptions()
-  const { status } = useStatus()
-  const params = useParams({
-    from: '/_authenticated/system-settings/maintenance/$section',
-  })
-
-  const settings = useMemo(
-    () => getOptionValue(data?.data, DEFAULT_MAINTENANCE_SETTINGS),
-    [data?.data]
-  )
-
-  if (isLoading) {
-    return (
-      <div className='text-muted-foreground flex h-full w-full flex-1 items-center justify-center'>
-        {t('Loading maintenance settings...')}
-      </div>
-    )
-  }
-
-  const activeSection = (params?.section ?? MAINTENANCE_DEFAULT_SECTION) as
-    | 'update-checker'
-    | 'notice'
-    | 'logs'
-    | 'header-navigation'
-    | 'sidebar-modules'
-    | 'performance'
-  const sectionContent = getMaintenanceSectionContent(
-    activeSection,
-    settings,
-    status?.version as string | undefined,
-    status?.start_time as number | null | undefined
-  )
-
-  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>
-  )
-}

+ 0 - 137
web/default/src/features/system-settings/maintenance/section-registry.tsx

@@ -1,137 +0,0 @@
-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

+ 7 - 0
web/default/src/features/system-settings/models/index.tsx

@@ -43,6 +43,13 @@ const defaultModelSettings: ModelSettings = {
   AutoGroups: '',
   AutoGroups: '',
   DefaultUseAutoGroup: false,
   DefaultUseAutoGroup: false,
   'group_ratio_setting.group_special_usable_group': '{}',
   '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() {
 export function ModelSettings() {

+ 1032 - 0
web/default/src/features/system-settings/models/model-pricing-sheet.tsx

@@ -0,0 +1,1032 @@
+import { useEffect, useMemo, useState } from 'react'
+import * as z from 'zod'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { AlertTriangle, ChevronDown } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+import { cn } from '@/lib/utils'
+import { Alert, AlertDescription } from '@/components/ui/alert'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import {
+  Collapsible,
+  CollapsibleContent,
+  CollapsibleTrigger,
+} from '@/components/ui/collapsible'
+import {
+  Field,
+  FieldContent,
+  FieldDescription,
+  FieldGroup,
+  FieldLabel,
+  FieldTitle,
+} from '@/components/ui/field'
+import {
+  Form,
+  FormControl,
+  FormDescription,
+  FormField,
+  FormItem,
+  FormLabel,
+  FormMessage,
+} from '@/components/ui/form'
+import { Input } from '@/components/ui/input'
+import {
+  InputGroup,
+  InputGroupAddon,
+  InputGroupInput,
+} from '@/components/ui/input-group'
+import {
+  Sheet,
+  SheetContent,
+  SheetDescription,
+  SheetFooter,
+  SheetHeader,
+  SheetTitle,
+} from '@/components/ui/sheet'
+import { Switch } from '@/components/ui/switch'
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { combineBillingExpr } from '@/features/pricing/lib/billing-expr'
+import { TieredPricingEditor } from './tiered-pricing-editor'
+
+const createModelPricingSchema = (t: (key: string) => string) =>
+  z.object({
+    name: z.string().min(1, t('Model name is required')),
+    price: z.string().optional(),
+    ratio: z.string().optional(),
+    cacheRatio: z.string().optional(),
+    createCacheRatio: z.string().optional(),
+    completionRatio: z.string().optional(),
+    imageRatio: z.string().optional(),
+    audioRatio: z.string().optional(),
+    audioCompletionRatio: z.string().optional(),
+  })
+
+type ModelPricingFormValues = z.infer<
+  ReturnType<typeof createModelPricingSchema>
+>
+
+type PricingMode = 'per-token' | 'per-request' | 'tiered_expr'
+type LaneKey =
+  | 'completion'
+  | 'cache'
+  | 'createCache'
+  | 'image'
+  | 'audioInput'
+  | 'audioOutput'
+
+export type ModelRatioData = {
+  name: string
+  price?: string
+  ratio?: string
+  cacheRatio?: string
+  createCacheRatio?: string
+  completionRatio?: string
+  imageRatio?: string
+  audioRatio?: string
+  audioCompletionRatio?: string
+  billingMode?: PricingMode
+  billingExpr?: string
+  requestRuleExpr?: string
+}
+
+type ModelPricingSheetProps = {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  onSave: (data: ModelRatioData) => void
+  onCancel?: () => void
+  editData?: ModelRatioData | null
+  selectedTargetCount?: number
+}
+
+type ModelPricingEditorPanelProps = Omit<
+  ModelPricingSheetProps,
+  'open' | 'onOpenChange'
+> & {
+  className?: string
+}
+
+type PreviewRow = {
+  key: string
+  label: string
+  value: string
+  multiline?: boolean
+}
+
+const numericDraftRegex = /^(\d+(\.\d*)?|\.\d*)?$/
+
+const EMPTY_LANE_PRICES: Record<LaneKey, string> = {
+  completion: '',
+  cache: '',
+  createCache: '',
+  image: '',
+  audioInput: '',
+  audioOutput: '',
+}
+
+const EMPTY_LANE_ENABLED: Record<LaneKey, boolean> = {
+  completion: false,
+  cache: false,
+  createCache: false,
+  image: false,
+  audioInput: false,
+  audioOutput: false,
+}
+
+const ratioFieldByLane: Record<LaneKey, keyof ModelPricingFormValues> = {
+  completion: 'completionRatio',
+  cache: 'cacheRatio',
+  createCache: 'createCacheRatio',
+  image: 'imageRatio',
+  audioInput: 'audioRatio',
+  audioOutput: 'audioCompletionRatio',
+}
+
+const laneConfigs: Array<{
+  key: LaneKey
+  titleKey: string
+  descriptionKey: string
+  placeholder: string
+}> = [
+  {
+    key: 'completion',
+    titleKey: 'Completion price',
+    descriptionKey: 'Output token price for generated tokens.',
+    placeholder: '15',
+  },
+  {
+    key: 'cache',
+    titleKey: 'Cache read price',
+    descriptionKey: 'Token price for cache reads.',
+    placeholder: '0.3',
+  },
+  {
+    key: 'createCache',
+    titleKey: 'Cache write price',
+    descriptionKey: 'Token price for creating cache entries.',
+    placeholder: '3.75',
+  },
+  {
+    key: 'image',
+    titleKey: 'Image input price',
+    descriptionKey: 'Token price for image input.',
+    placeholder: '2.5',
+  },
+  {
+    key: 'audioInput',
+    titleKey: 'Audio input price',
+    descriptionKey: 'Token price for audio input.',
+    placeholder: '3.81',
+  },
+  {
+    key: 'audioOutput',
+    titleKey: 'Audio output price',
+    descriptionKey: 'Token price for audio output.',
+    placeholder: '15.11',
+  },
+]
+
+function hasValue(value: unknown): boolean {
+  return (
+    value !== '' && value !== null && value !== undefined && value !== false
+  )
+}
+
+function toNumberOrNull(value: unknown): number | null {
+  if (!hasValue(value) && value !== 0) return null
+  const num = Number(value)
+  return Number.isFinite(num) ? num : null
+}
+
+function formatNumber(value: unknown): string {
+  const num = toNumberOrNull(value)
+  if (num === null) return ''
+  return Number.parseFloat(num.toFixed(12)).toString()
+}
+
+function ratioToBasePrice(ratio: unknown): string {
+  const num = toNumberOrNull(ratio)
+  if (num === null) return ''
+  return formatNumber(num * 2)
+}
+
+function deriveLanePrice(
+  ratio: unknown,
+  denominator: unknown,
+  fallback = ''
+): string {
+  const ratioNumber = toNumberOrNull(ratio)
+  const denominatorNumber = toNumberOrNull(denominator)
+  if (ratioNumber === null || denominatorNumber === null) return fallback
+  return formatNumber(ratioNumber * denominatorNumber)
+}
+
+function createInitialLaneState(data?: ModelRatioData | null) {
+  if (!data) {
+    return {
+      promptPrice: '',
+      prices: { ...EMPTY_LANE_PRICES },
+      enabled: { ...EMPTY_LANE_ENABLED },
+    }
+  }
+
+  const promptPrice = ratioToBasePrice(data.ratio)
+  const audioInputPrice = deriveLanePrice(data.audioRatio, promptPrice)
+  const prices: Record<LaneKey, string> = {
+    completion: deriveLanePrice(data.completionRatio, promptPrice),
+    cache: deriveLanePrice(data.cacheRatio, promptPrice),
+    createCache: deriveLanePrice(data.createCacheRatio, promptPrice),
+    image: deriveLanePrice(data.imageRatio, promptPrice),
+    audioInput: audioInputPrice,
+    audioOutput: deriveLanePrice(data.audioCompletionRatio, audioInputPrice),
+  }
+
+  return {
+    promptPrice,
+    prices,
+    enabled: {
+      completion: hasValue(data.completionRatio),
+      cache: hasValue(data.cacheRatio),
+      createCache: hasValue(data.createCacheRatio),
+      image: hasValue(data.imageRatio),
+      audioInput: hasValue(data.audioRatio),
+      audioOutput: hasValue(data.audioCompletionRatio),
+    },
+  }
+}
+
+function getModeLabel(mode: PricingMode) {
+  if (mode === 'per-request') return 'Per-request'
+  if (mode === 'tiered_expr') return 'Expression'
+  return 'Per-token'
+}
+
+function getModeBadgeVariant(
+  mode: PricingMode
+): 'default' | 'secondary' | 'outline' {
+  if (mode === 'per-request') return 'secondary'
+  if (mode === 'tiered_expr') return 'default'
+  return 'outline'
+}
+
+function truncateExpr(value: string) {
+  if (!value) return ''
+  return value.length > 110 ? `${value.slice(0, 110)}...` : value
+}
+
+function buildPreviewRows(
+  values: ModelPricingFormValues,
+  mode: PricingMode,
+  billingExpr: string,
+  requestRuleExpr: string,
+  promptPrice: string,
+  lanePrices: Record<LaneKey, string>,
+  laneEnabled: Record<LaneKey, boolean>,
+  t: (key: string) => string
+): PreviewRow[] {
+  if (mode === 'tiered_expr') {
+    const effectiveExpr = combineBillingExpr(billingExpr, requestRuleExpr)
+    return [
+      { key: 'mode', label: 'BillingMode', value: 'tiered_expr' },
+      {
+        key: 'expr',
+        label: t('Expression'),
+        value: effectiveExpr || t('Empty'),
+        multiline: true,
+      },
+    ]
+  }
+
+  if (mode === 'per-request') {
+    return [
+      {
+        key: 'price',
+        label: 'ModelPrice',
+        value: values.price || t('Empty'),
+      },
+    ]
+  }
+
+  return [
+    {
+      key: 'inputPrice',
+      label: t('Input price'),
+      value: promptPrice ? `$${promptPrice}` : t('Empty'),
+    },
+    {
+      key: 'completion',
+      label: t('Completion price'),
+      value:
+        laneEnabled.completion && lanePrices.completion
+          ? `$${lanePrices.completion}`
+          : t('Empty'),
+    },
+    {
+      key: 'cache',
+      label: t('Cache read price'),
+      value:
+        laneEnabled.cache && lanePrices.cache
+          ? `$${lanePrices.cache}`
+          : t('Empty'),
+    },
+    {
+      key: 'createCache',
+      label: t('Cache write price'),
+      value:
+        laneEnabled.createCache && lanePrices.createCache
+          ? `$${lanePrices.createCache}`
+          : t('Empty'),
+    },
+    {
+      key: 'image',
+      label: t('Image input price'),
+      value:
+        laneEnabled.image && lanePrices.image
+          ? `$${lanePrices.image}`
+          : t('Empty'),
+    },
+    {
+      key: 'audio',
+      label: t('Audio input price'),
+      value:
+        laneEnabled.audioInput && lanePrices.audioInput
+          ? `$${lanePrices.audioInput}`
+          : t('Empty'),
+    },
+    {
+      key: 'audioCompletion',
+      label: t('Audio output price'),
+      value:
+        laneEnabled.audioOutput && lanePrices.audioOutput
+          ? `$${lanePrices.audioOutput}`
+          : t('Empty'),
+    },
+  ]
+}
+
+export function ModelPricingSheet({
+  open,
+  onOpenChange,
+  onSave,
+  onCancel,
+  editData,
+  selectedTargetCount = 0,
+}: ModelPricingSheetProps) {
+  const { t } = useTranslation()
+  const title = editData ? t('Edit model pricing') : t('Add model pricing')
+  const description = editData?.name || t('New model')
+
+  return (
+    <Sheet open={open} onOpenChange={onOpenChange}>
+      <SheetContent side='right' className='w-full gap-0 p-0 sm:max-w-2xl'>
+        <SheetHeader className='sr-only'>
+          <SheetTitle>{title}</SheetTitle>
+          <SheetDescription>{description}</SheetDescription>
+        </SheetHeader>
+        <ModelPricingEditorPanel
+          onSave={onSave}
+          editData={editData}
+          selectedTargetCount={selectedTargetCount}
+          onCancel={() => {
+            onCancel?.()
+            onOpenChange(false)
+          }}
+          className='h-full rounded-none border-0'
+        />
+      </SheetContent>
+    </Sheet>
+  )
+}
+
+export function ModelPricingEditorPanel({
+  onSave,
+  editData,
+  selectedTargetCount = 0,
+  onCancel,
+  className,
+}: ModelPricingEditorPanelProps) {
+  const { t } = useTranslation()
+  const [pricingMode, setPricingMode] = useState<PricingMode>('per-token')
+  const [promptPrice, setPromptPrice] = useState('')
+  const [lanePrices, setLanePrices] = useState<Record<LaneKey, string>>({
+    ...EMPTY_LANE_PRICES,
+  })
+  const [laneEnabled, setLaneEnabled] = useState<Record<LaneKey, boolean>>({
+    ...EMPTY_LANE_ENABLED,
+  })
+  const [billingExpr, setBillingExpr] = useState('')
+  const [requestRuleExpr, setRequestRuleExpr] = useState('')
+  const [previewOpen, setPreviewOpen] = useState(true)
+  const isEditMode = !!editData
+
+  const form = useForm<ModelPricingFormValues>({
+    resolver: zodResolver(createModelPricingSchema(t)),
+    defaultValues: {
+      name: '',
+      price: '',
+      ratio: '',
+      cacheRatio: '',
+      createCacheRatio: '',
+      completionRatio: '',
+      imageRatio: '',
+      audioRatio: '',
+      audioCompletionRatio: '',
+    },
+  })
+
+  useEffect(() => {
+    const nextLaneState = createInitialLaneState(editData)
+
+    if (editData) {
+      form.reset({
+        name: editData.name,
+        price: editData.price || '',
+        ratio: editData.ratio || '',
+        cacheRatio: editData.cacheRatio || '',
+        createCacheRatio: editData.createCacheRatio || '',
+        completionRatio: editData.completionRatio || '',
+        imageRatio: editData.imageRatio || '',
+        audioRatio: editData.audioRatio || '',
+        audioCompletionRatio: editData.audioCompletionRatio || '',
+      })
+      setPricingMode(
+        editData.billingMode === 'tiered_expr'
+          ? 'tiered_expr'
+          : editData.price
+            ? 'per-request'
+            : 'per-token'
+      )
+      setBillingExpr(editData.billingExpr || '')
+      setRequestRuleExpr(editData.requestRuleExpr || '')
+    } else {
+      form.reset({
+        name: '',
+        price: '',
+        ratio: '',
+        cacheRatio: '',
+        createCacheRatio: '',
+        completionRatio: '',
+        imageRatio: '',
+        audioRatio: '',
+        audioCompletionRatio: '',
+      })
+      setPricingMode('per-token')
+      setBillingExpr('')
+      setRequestRuleExpr('')
+    }
+
+    setPromptPrice(nextLaneState.promptPrice)
+    setLanePrices(nextLaneState.prices)
+    setLaneEnabled(nextLaneState.enabled)
+    setPreviewOpen(true)
+  }, [editData, form])
+
+  const setFormValue = (field: keyof ModelPricingFormValues, value: string) => {
+    form.setValue(field, value, {
+      shouldDirty: true,
+      shouldValidate: true,
+    })
+  }
+
+  const deriveLaneRatio = (
+    lane: LaneKey,
+    price: string,
+    nextPromptPrice = promptPrice,
+    nextLanePrices = lanePrices
+  ) => {
+    const priceNumber = toNumberOrNull(price)
+    if (priceNumber === null) return ''
+
+    if (lane === 'audioOutput') {
+      const audioInputPrice = toNumberOrNull(nextLanePrices.audioInput)
+      if (audioInputPrice === null || audioInputPrice === 0) return ''
+      return formatNumber(priceNumber / audioInputPrice)
+    }
+
+    const inputPrice = toNumberOrNull(nextPromptPrice)
+    if (inputPrice === null || inputPrice === 0) return ''
+    return formatNumber(priceNumber / inputPrice)
+  }
+
+  const syncLaneRatios = (
+    nextPromptPrice = promptPrice,
+    nextLanePrices = lanePrices,
+    nextLaneEnabled = laneEnabled
+  ) => {
+    const inputPrice = toNumberOrNull(nextPromptPrice)
+    setFormValue(
+      'ratio',
+      inputPrice !== null ? formatNumber(inputPrice / 2) : ''
+    )
+
+    laneConfigs.forEach(({ key }) => {
+      const ratioField = ratioFieldByLane[key]
+      if (!nextLaneEnabled[key]) {
+        setFormValue(ratioField, '')
+        return
+      }
+      setFormValue(
+        ratioField,
+        deriveLaneRatio(
+          key,
+          nextLanePrices[key],
+          nextPromptPrice,
+          nextLanePrices
+        )
+      )
+    })
+  }
+
+  const handlePromptPriceChange = (value: string) => {
+    if (!numericDraftRegex.test(value)) return
+    setPromptPrice(value)
+    syncLaneRatios(value, lanePrices, laneEnabled)
+  }
+
+  const handleLanePriceChange = (lane: LaneKey, value: string) => {
+    if (!numericDraftRegex.test(value)) return
+    const nextLanePrices = { ...lanePrices, [lane]: value }
+    setLanePrices(nextLanePrices)
+
+    if (laneEnabled[lane]) {
+      setFormValue(
+        ratioFieldByLane[lane],
+        deriveLaneRatio(lane, value, promptPrice, nextLanePrices)
+      )
+    }
+
+    if (lane === 'audioInput' && laneEnabled.audioOutput) {
+      setFormValue(
+        'audioCompletionRatio',
+        deriveLaneRatio(
+          'audioOutput',
+          nextLanePrices.audioOutput,
+          promptPrice,
+          nextLanePrices
+        )
+      )
+    }
+  }
+
+  const handleLaneToggle = (lane: LaneKey, checked: boolean) => {
+    const nextEnabled = { ...laneEnabled, [lane]: checked }
+    let nextPrices = lanePrices
+
+    if (!checked) {
+      nextPrices = { ...nextPrices, [lane]: '' }
+      setFormValue(ratioFieldByLane[lane], '')
+      if (lane === 'audioInput') {
+        nextEnabled.audioOutput = false
+        nextPrices.audioOutput = ''
+        setFormValue('audioCompletionRatio', '')
+      }
+    }
+
+    setLaneEnabled(nextEnabled)
+    setLanePrices(nextPrices)
+
+    if (checked) {
+      setFormValue(
+        ratioFieldByLane[lane],
+        deriveLaneRatio(lane, nextPrices[lane], promptPrice, nextPrices)
+      )
+    }
+  }
+
+  const handleModeChange = (value: string) => {
+    const nextMode = value as PricingMode
+    setPricingMode(nextMode)
+    if (nextMode === 'tiered_expr' && !billingExpr) {
+      setBillingExpr('tier("base", p * 0 + c * 0)')
+    }
+  }
+
+  const watchedValues = form.watch()
+  const previewRows = useMemo(
+    () =>
+      buildPreviewRows(
+        watchedValues,
+        pricingMode,
+        billingExpr,
+        requestRuleExpr,
+        promptPrice,
+        lanePrices,
+        laneEnabled,
+        t
+      ),
+    [
+      billingExpr,
+      laneEnabled,
+      lanePrices,
+      pricingMode,
+      promptPrice,
+      requestRuleExpr,
+      t,
+      watchedValues,
+    ]
+  )
+
+  const warnings = useMemo(() => {
+    const nextWarnings: string[] = []
+    const hasConflict =
+      !!editData?.price &&
+      [
+        editData.ratio,
+        editData.completionRatio,
+        editData.cacheRatio,
+        editData.createCacheRatio,
+        editData.imageRatio,
+        editData.audioRatio,
+        editData.audioCompletionRatio,
+      ].some(hasValue)
+
+    if (hasConflict) {
+      nextWarnings.push(
+        t(
+          'This model has both fixed-price and token-price settings. Saving the current mode will rewrite the conflicting fields.'
+        )
+      )
+    }
+
+    if (
+      pricingMode === 'per-token' &&
+      toNumberOrNull(promptPrice) === null &&
+      laneConfigs.some(
+        ({ key }) => laneEnabled[key] && hasValue(lanePrices[key])
+      )
+    ) {
+      nextWarnings.push(
+        t('Input price is required before saving dependent prices.')
+      )
+    }
+
+    if (
+      pricingMode === 'per-token' &&
+      laneEnabled.audioOutput &&
+      !hasValue(lanePrices.audioInput)
+    ) {
+      nextWarnings.push(t('Audio output price requires an audio input price.'))
+    }
+
+    return nextWarnings
+  }, [editData, laneEnabled, lanePrices, pricingMode, promptPrice, t])
+
+  const handleSubmit = (values: ModelPricingFormValues) => {
+    if (
+      pricingMode === 'per-token' &&
+      toNumberOrNull(promptPrice) === null &&
+      laneConfigs.some(
+        ({ key }) => laneEnabled[key] && hasValue(lanePrices[key])
+      )
+    ) {
+      form.setError('ratio', {
+        message: t('Input price is required before saving dependent prices.'),
+      })
+      return
+    }
+
+    if (
+      pricingMode === 'per-token' &&
+      laneEnabled.audioOutput &&
+      !hasValue(lanePrices.audioInput)
+    ) {
+      form.setError('audioRatio', {
+        message: t('Audio output price requires an audio input price.'),
+      })
+      return
+    }
+
+    const data: ModelRatioData = {
+      name: values.name.trim(),
+      billingMode: pricingMode,
+      price: values.price || '',
+      ratio: values.ratio || '',
+      cacheRatio: values.cacheRatio || '',
+      createCacheRatio: values.createCacheRatio || '',
+      completionRatio: values.completionRatio || '',
+      imageRatio: values.imageRatio || '',
+      audioRatio: values.audioRatio || '',
+      audioCompletionRatio: values.audioCompletionRatio || '',
+    }
+
+    if (pricingMode === 'tiered_expr') {
+      data.billingExpr = billingExpr
+      data.requestRuleExpr = requestRuleExpr
+    }
+
+    onSave(data)
+    form.reset()
+    onCancel?.()
+  }
+
+  const activeName = watchedValues.name || editData?.name || t('New model')
+
+  return (
+    <div
+      className={cn(
+        'bg-card flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border',
+        className
+      )}
+    >
+      <div className='border-b p-4'>
+        <div className='flex flex-wrap items-start justify-between gap-3'>
+          <div className='min-w-0'>
+            <h3 className='truncate text-base font-medium'>
+              {isEditMode ? t('Edit model pricing') : t('Add model pricing')}
+            </h3>
+            <p className='text-muted-foreground truncate text-sm'>
+              {activeName}
+            </p>
+          </div>
+          <Badge variant={getModeBadgeVariant(pricingMode)}>
+            {t(getModeLabel(pricingMode))}
+          </Badge>
+        </div>
+      </div>
+
+      <Form {...form}>
+        <form
+          onSubmit={form.handleSubmit(handleSubmit)}
+          className='flex min-h-0 flex-1 flex-col'
+          autoComplete='off'
+        >
+          <div className='min-h-0 flex-1 overflow-y-auto p-4'>
+            <FieldGroup>
+              {warnings.length > 0 && (
+                <Alert variant='destructive'>
+                  <AlertTriangle data-icon='inline-start' />
+                  <AlertDescription>
+                    <div className='flex flex-col gap-1'>
+                      {warnings.map((warning) => (
+                        <span key={warning}>{warning}</span>
+                      ))}
+                    </div>
+                  </AlertDescription>
+                </Alert>
+              )}
+
+              <FormField
+                control={form.control}
+                name='name'
+                render={({ field }) => (
+                  <FormItem>
+                    <FormLabel>{t('Model name')}</FormLabel>
+                    <FormControl>
+                      <Input
+                        placeholder={t('gpt-4')}
+                        {...field}
+                        disabled={isEditMode}
+                      />
+                    </FormControl>
+                    <FormDescription>
+                      {t('The exact model identifier as used in API requests.')}
+                    </FormDescription>
+                    <FormMessage />
+                  </FormItem>
+                )}
+              />
+
+              <Tabs value={pricingMode} onValueChange={handleModeChange}>
+                <TabsList className='grid w-full grid-cols-3'>
+                  <TabsTrigger value='per-token'>{t('Per-token')}</TabsTrigger>
+                  <TabsTrigger value='per-request'>
+                    {t('Per-request')}
+                  </TabsTrigger>
+                  <TabsTrigger value='tiered_expr'>
+                    {t('Expression')}
+                  </TabsTrigger>
+                </TabsList>
+
+                <TabsContent value='per-token' className='flex flex-col gap-5'>
+                  <FieldGroup>
+                    <Field>
+                      <FieldLabel>{t('Input price')}</FieldLabel>
+                      <PriceInput
+                        value={promptPrice}
+                        placeholder='3'
+                        onChange={handlePromptPriceChange}
+                      />
+                      <FieldDescription>
+                        {t('USD price per 1M input tokens.')}
+                      </FieldDescription>
+                    </Field>
+
+                    <div className='grid gap-3 sm:grid-cols-2'>
+                      {laneConfigs.map((lane) => {
+                        const disabled =
+                          lane.key === 'audioOutput' &&
+                          (!laneEnabled.audioInput ||
+                            !hasValue(lanePrices.audioInput))
+                        return (
+                          <PriceLane
+                            key={lane.key}
+                            title={t(lane.titleKey)}
+                            description={t(lane.descriptionKey)}
+                            placeholder={lane.placeholder}
+                            value={lanePrices[lane.key]}
+                            enabled={laneEnabled[lane.key]}
+                            disabled={disabled}
+                            onEnabledChange={(checked) =>
+                              handleLaneToggle(lane.key, checked)
+                            }
+                            onChange={(value) =>
+                              handleLanePriceChange(lane.key, value)
+                            }
+                          />
+                        )
+                      })}
+                    </div>
+                  </FieldGroup>
+                </TabsContent>
+
+                <TabsContent
+                  value='per-request'
+                  className='flex flex-col gap-5'
+                >
+                  <FormField
+                    control={form.control}
+                    name='price'
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>{t('Fixed price')}</FormLabel>
+                        <FormControl>
+                          <InputGroup>
+                            <InputGroupAddon>$</InputGroupAddon>
+                            <InputGroupInput
+                              inputMode='decimal'
+                              placeholder='0.01'
+                              {...field}
+                              onChange={(event) => {
+                                const value = event.target.value
+                                if (numericDraftRegex.test(value)) {
+                                  field.onChange(value)
+                                }
+                              }}
+                            />
+                            <InputGroupAddon align='inline-end'>
+                              {t('per request')}
+                            </InputGroupAddon>
+                          </InputGroup>
+                        </FormControl>
+                        <FormDescription>
+                          {t(
+                            'Cost in USD per request, regardless of tokens used.'
+                          )}
+                        </FormDescription>
+                        <FormMessage />
+                      </FormItem>
+                    )}
+                  />
+                </TabsContent>
+
+                <TabsContent
+                  value='tiered_expr'
+                  className='flex flex-col gap-5'
+                >
+                  <TieredPricingEditor
+                    modelName={watchedValues.name}
+                    billingExpr={billingExpr}
+                    requestRuleExpr={requestRuleExpr}
+                    onBillingExprChange={setBillingExpr}
+                    onRequestRuleExprChange={setRequestRuleExpr}
+                  />
+                </TabsContent>
+              </Tabs>
+
+              <Collapsible open={previewOpen} onOpenChange={setPreviewOpen}>
+                <CollapsibleTrigger
+                  render={
+                    <Button
+                      type='button'
+                      variant='outline'
+                      className='flex w-full justify-between'
+                    />
+                  }
+                >
+                  <span>{t('Save preview')}</span>
+                  <ChevronDown
+                    className={cn(
+                      'transition-transform',
+                      previewOpen && 'rotate-180'
+                    )}
+                  />
+                </CollapsibleTrigger>
+                <CollapsibleContent className='pt-3'>
+                  <div className='rounded-lg border'>
+                    {previewRows.map((row) => (
+                      <div
+                        key={row.key}
+                        className='grid grid-cols-[140px_1fr] gap-3 border-b px-3 py-2 text-sm last:border-b-0'
+                      >
+                        <span className='text-muted-foreground text-xs'>
+                          {row.label}
+                        </span>
+                        <span
+                          className={cn(
+                            'min-w-0',
+                            row.multiline
+                              ? 'font-mono text-xs leading-5 break-words whitespace-pre-wrap'
+                              : 'truncate'
+                          )}
+                        >
+                          {row.value}
+                        </span>
+                      </div>
+                    ))}
+                  </div>
+                </CollapsibleContent>
+              </Collapsible>
+            </FieldGroup>
+          </div>
+
+          <SheetFooter className='bg-background/95 border-t sm:flex-row sm:items-center sm:justify-between'>
+            <div className='text-muted-foreground text-xs'>
+              {selectedTargetCount > 0
+                ? t('{{count}} selected targets available for bulk copy.', {
+                    count: selectedTargetCount,
+                  })
+                : t('Changes are written to the settings draft on save.')}
+            </div>
+            <div className='flex justify-end gap-2'>
+              <Button type='button' variant='outline' onClick={onCancel}>
+                {t('Cancel')}
+              </Button>
+              <Button type='submit'>
+                {isEditMode ? t('Update') : t('Add')}
+              </Button>
+            </div>
+          </SheetFooter>
+        </form>
+      </Form>
+    </div>
+  )
+}
+
+function PriceInput(props: {
+  value: string
+  placeholder?: string
+  disabled?: boolean
+  onChange: (value: string) => void
+}) {
+  return (
+    <InputGroup>
+      <InputGroupAddon>$</InputGroupAddon>
+      <InputGroupInput
+        inputMode='decimal'
+        value={props.value}
+        placeholder={props.placeholder}
+        disabled={props.disabled}
+        onChange={(event) => props.onChange(event.target.value)}
+      />
+      <InputGroupAddon align='inline-end'>$/1M</InputGroupAddon>
+    </InputGroup>
+  )
+}
+
+function PriceLane(props: {
+  title: string
+  description: string
+  placeholder: string
+  value: string
+  enabled: boolean
+  disabled?: boolean
+  onEnabledChange: (checked: boolean) => void
+  onChange: (value: string) => void
+}) {
+  const { t } = useTranslation()
+  const effectiveDisabled = props.disabled || !props.enabled
+
+  return (
+    <Field
+      className={cn(
+        'rounded-lg border p-3',
+        effectiveDisabled && 'bg-muted/35'
+      )}
+      data-disabled={effectiveDisabled || undefined}
+    >
+      <div className='flex items-start justify-between gap-3'>
+        <FieldContent>
+          <FieldTitle>{props.title}</FieldTitle>
+          <FieldDescription>{props.description}</FieldDescription>
+        </FieldContent>
+        <Switch
+          checked={props.enabled}
+          disabled={props.disabled}
+          onCheckedChange={props.onEnabledChange}
+          aria-label={props.title}
+        />
+      </div>
+      <PriceInput
+        value={props.value}
+        placeholder={props.placeholder}
+        disabled={effectiveDisabled}
+        onChange={props.onChange}
+      />
+      <FieldDescription>
+        {props.enabled
+          ? t('USD price per 1M tokens.')
+          : t('Disabled lanes are omitted on save.')}
+      </FieldDescription>
+    </Field>
+  )
+}

+ 0 - 656
web/default/src/features/system-settings/models/model-ratio-dialog.tsx

@@ -1,656 +0,0 @@
-import { useEffect, useState } from 'react'
-import * as z from 'zod'
-import { useForm } from 'react-hook-form'
-import { zodResolver } from '@hookform/resolvers/zod'
-import { ChevronDown } from 'lucide-react'
-import { useTranslation } from 'react-i18next'
-import { Button } from '@/components/ui/button'
-import {
-  Collapsible,
-  CollapsibleContent,
-  CollapsibleTrigger,
-} from '@/components/ui/collapsible'
-import {
-  Dialog,
-  DialogContent,
-  DialogDescription,
-  DialogFooter,
-  DialogHeader,
-  DialogTitle,
-} from '@/components/ui/dialog'
-import {
-  Form,
-  FormControl,
-  FormDescription,
-  FormField,
-  FormItem,
-  FormLabel,
-  FormMessage,
-} from '@/components/ui/form'
-import { Input } from '@/components/ui/input'
-import { Label } from '@/components/ui/label'
-import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
-import { TieredPricingEditor } from './tiered-pricing-editor'
-
-const createModelDialogSchema = (t: (key: string) => string) =>
-  z.object({
-    name: z.string().min(1, t('Model name is required')),
-    price: z.string().optional(),
-    ratio: z.string().optional(),
-    cacheRatio: z.string().optional(),
-    createCacheRatio: z.string().optional(),
-    completionRatio: z.string().optional(),
-    imageRatio: z.string().optional(),
-    audioRatio: z.string().optional(),
-    audioCompletionRatio: z.string().optional(),
-  })
-
-type ModelDialogFormValues = z.infer<ReturnType<typeof createModelDialogSchema>>
-
-type PricingMode = 'per-token' | 'per-request' | 'tiered_expr'
-type PricingSubMode = 'ratio' | 'price'
-
-export type ModelRatioData = {
-  name: string
-  price?: string
-  ratio?: string
-  cacheRatio?: string
-  createCacheRatio?: string
-  completionRatio?: string
-  imageRatio?: string
-  audioRatio?: string
-  audioCompletionRatio?: string
-  billingMode?: 'per-token' | 'per-request' | 'tiered_expr'
-  billingExpr?: string
-  requestRuleExpr?: string
-}
-
-type ModelRatioDialogProps = {
-  open: boolean
-  onOpenChange: (open: boolean) => void
-  onSave: (data: ModelRatioData) => void
-  editData?: ModelRatioData | null
-}
-
-export function ModelRatioDialog({
-  open,
-  onOpenChange,
-  onSave,
-  editData,
-}: ModelRatioDialogProps) {
-  const { t } = useTranslation()
-  const [pricingMode, setPricingMode] = useState<PricingMode>('per-token')
-  const [pricingSubMode, setPricingSubMode] = useState<PricingSubMode>('ratio')
-  const [advancedOpen, setAdvancedOpen] = useState(false)
-  const [promptPrice, setPromptPrice] = useState('')
-  const [completionPrice, setCompletionPrice] = useState('')
-  const [billingExpr, setBillingExpr] = useState('')
-  const [requestRuleExpr, setRequestRuleExpr] = useState('')
-  const isEditMode = !!editData
-
-  const form = useForm<ModelDialogFormValues>({
-    resolver: zodResolver(createModelDialogSchema(t)),
-    defaultValues: {
-      name: '',
-      price: '',
-      ratio: '',
-      cacheRatio: '',
-      createCacheRatio: '',
-      completionRatio: '',
-      imageRatio: '',
-      audioRatio: '',
-      audioCompletionRatio: '',
-    },
-  })
-
-  useEffect(() => {
-    if (editData) {
-      form.reset(editData)
-      // eslint-disable-next-line react-hooks/set-state-in-effect
-      setBillingExpr(editData.billingExpr || '')
-      setRequestRuleExpr(editData.requestRuleExpr || '')
-
-      if (editData.billingMode === 'tiered_expr') {
-        setPricingMode('tiered_expr')
-      } else if (editData.price && editData.price !== '') {
-        setPricingMode('per-request')
-      } else {
-        setPricingMode('per-token')
-        if (editData.ratio) {
-          const tokenPrice = parseFloat(editData.ratio) * 2
-          setPromptPrice(tokenPrice.toString())
-          if (editData.completionRatio) {
-            const compPrice = tokenPrice * parseFloat(editData.completionRatio)
-            setCompletionPrice(compPrice.toString())
-          }
-        }
-      }
-    } else {
-      form.reset({
-        name: '',
-        price: '',
-        ratio: '',
-        cacheRatio: '',
-        createCacheRatio: '',
-        completionRatio: '',
-        imageRatio: '',
-        audioRatio: '',
-        audioCompletionRatio: '',
-      })
-      setPricingMode('per-token')
-      setPricingSubMode('ratio')
-      setPromptPrice('')
-      setCompletionPrice('')
-      setBillingExpr('')
-      setRequestRuleExpr('')
-      setAdvancedOpen(false)
-    }
-  }, [editData, form, open])
-
-  const handleSubmit = (values: ModelDialogFormValues) => {
-    // Always pass through every field. The visual editor decides what to
-    // persist based on `billingMode`, and tiered_expr models also keep the
-    // ratio/price values as fallback during multi-instance sync delays
-    // (the backend's ModelPriceHelper checks billing_mode first, so these
-    // fallbacks only kick in when billing_setting hasn't propagated yet).
-    const data: ModelRatioData = {
-      name: values.name,
-      billingMode: pricingMode,
-      price: values.price || '',
-      ratio: values.ratio || '',
-      cacheRatio: values.cacheRatio || '',
-      createCacheRatio: values.createCacheRatio || '',
-      completionRatio: values.completionRatio || '',
-      imageRatio: values.imageRatio || '',
-      audioRatio: values.audioRatio || '',
-      audioCompletionRatio: values.audioCompletionRatio || '',
-    }
-
-    if (pricingMode === 'tiered_expr') {
-      data.billingExpr = billingExpr
-      data.requestRuleExpr = requestRuleExpr
-    }
-
-    onSave(data)
-    form.reset()
-    onOpenChange(false)
-  }
-
-  const validateNumber = (value: string) => {
-    if (value === '') return true
-    return !isNaN(parseFloat(value))
-  }
-
-  const handlePromptPriceChange = (value: string) => {
-    setPromptPrice(value)
-    if (value && !isNaN(parseFloat(value))) {
-      const ratio = parseFloat(value) / 2
-      form.setValue('ratio', ratio.toString())
-    } else {
-      form.setValue('ratio', '')
-    }
-  }
-
-  const handleCompletionPriceChange = (value: string) => {
-    setCompletionPrice(value)
-    if (
-      value &&
-      !isNaN(parseFloat(value)) &&
-      promptPrice &&
-      !isNaN(parseFloat(promptPrice)) &&
-      parseFloat(promptPrice) > 0
-    ) {
-      const completionRatio = parseFloat(value) / parseFloat(promptPrice)
-      form.setValue('completionRatio', completionRatio.toString())
-    } else {
-      form.setValue('completionRatio', '')
-    }
-  }
-
-  return (
-    <Dialog open={open} onOpenChange={onOpenChange}>
-      <DialogContent className='max-h-[90vh] overflow-y-auto sm:max-w-[680px]'>
-        <DialogHeader>
-          <DialogTitle>
-            {isEditMode ? t('Edit model') : t('Add model')}
-          </DialogTitle>
-          <DialogDescription>
-            {t('Configure pricing ratios for a specific model.')}
-          </DialogDescription>
-        </DialogHeader>
-
-        <Form {...form}>
-          <form
-            onSubmit={form.handleSubmit(handleSubmit)}
-            className='space-y-6'
-            autoComplete='off'
-          >
-            <FormField
-              control={form.control}
-              name='name'
-              render={({ field }) => (
-                <FormItem>
-                  <FormLabel>{t('Model name')}</FormLabel>
-                  <FormControl>
-                    <Input
-                      placeholder={t('gpt-4')}
-                      {...field}
-                      disabled={isEditMode}
-                    />
-                  </FormControl>
-                  <FormDescription>
-                    {t('The exact model identifier as used in API requests.')}
-                  </FormDescription>
-                  <FormMessage />
-                </FormItem>
-              )}
-            />
-
-            <div className='space-y-4'>
-              <Label>{t('Pricing mode')}</Label>
-              <RadioGroup
-                value={pricingMode}
-                onValueChange={(value) => setPricingMode(value as PricingMode)}
-              >
-                <div className='flex items-center space-x-2'>
-                  <RadioGroupItem value='per-token' id='per-token' />
-                  <Label htmlFor='per-token' className='font-normal'>
-                    {t('Per-token (ratio based)')}
-                  </Label>
-                </div>
-                <div className='flex items-center space-x-2'>
-                  <RadioGroupItem value='per-request' id='per-request' />
-                  <Label htmlFor='per-request' className='font-normal'>
-                    {t('Per-request (fixed price)')}
-                  </Label>
-                </div>
-                <div className='flex items-center space-x-2'>
-                  <RadioGroupItem value='tiered_expr' id='tiered_expr' />
-                  <Label htmlFor='tiered_expr' className='font-normal'>
-                    {t('Tiered (billing expression)')}
-                  </Label>
-                </div>
-              </RadioGroup>
-            </div>
-
-            {pricingMode === 'tiered_expr' ? (
-              <TieredPricingEditor
-                modelName={form.getValues('name')}
-                billingExpr={billingExpr}
-                requestRuleExpr={requestRuleExpr}
-                onBillingExprChange={setBillingExpr}
-                onRequestRuleExprChange={setRequestRuleExpr}
-              />
-            ) : pricingMode === 'per-request' ? (
-              <FormField
-                control={form.control}
-                name='price'
-                render={({ field }) => (
-                  <FormItem>
-                    <FormLabel>{t('Fixed price (USD)')}</FormLabel>
-                    <FormControl>
-                      <Input
-                        type='text'
-                        placeholder='0.01'
-                        {...field}
-                        onChange={(e) => {
-                          const value = e.target.value
-                          if (validateNumber(value)) {
-                            field.onChange(value)
-                          }
-                        }}
-                      />
-                    </FormControl>
-                    <FormDescription>
-                      {t('Cost in USD per request, regardless of tokens used.')}
-                    </FormDescription>
-                    <FormMessage />
-                  </FormItem>
-                )}
-              />
-            ) : (
-              <>
-                <div className='space-y-4'>
-                  <Label>{t('Input mode')}</Label>
-                  <RadioGroup
-                    value={pricingSubMode}
-                    onValueChange={(value) =>
-                      setPricingSubMode(value as PricingSubMode)
-                    }
-                  >
-                    <div className='flex items-center space-x-2'>
-                      <RadioGroupItem value='ratio' id='ratio' />
-                      <Label htmlFor='ratio' className='font-normal'>
-                        {t('Ratio mode')}
-                      </Label>
-                    </div>
-                    <div className='flex items-center space-x-2'>
-                      <RadioGroupItem value='price' id='price' />
-                      <Label htmlFor='price' className='font-normal'>
-                        {t('Price mode (USD per 1M tokens)')}
-                      </Label>
-                    </div>
-                  </RadioGroup>
-                </div>
-
-                {pricingSubMode === 'ratio' ? (
-                  <>
-                    <FormField
-                      control={form.control}
-                      name='ratio'
-                      render={({ field }) => (
-                        <FormItem>
-                          <FormLabel>{t('Model ratio')}</FormLabel>
-                          <FormControl>
-                            <Input
-                              type='text'
-                              placeholder='1.0'
-                              {...field}
-                              onChange={(e) => {
-                                const value = e.target.value
-                                if (validateNumber(value)) {
-                                  field.onChange(value)
-                                  if (value) {
-                                    setPromptPrice(
-                                      (parseFloat(value) * 2).toString()
-                                    )
-                                  } else {
-                                    setPromptPrice('')
-                                  }
-                                }
-                              }}
-                            />
-                          </FormControl>
-                          <FormDescription>
-                            {field.value && !isNaN(parseFloat(field.value))
-                              ? t(
-                                  'Calculated price: ${{price}} per 1M tokens',
-                                  {
-                                    price: (
-                                      parseFloat(field.value) * 2
-                                    ).toFixed(4),
-                                  }
-                                )
-                              : t('Multiplier for prompt tokens.')}
-                          </FormDescription>
-                          <FormMessage />
-                        </FormItem>
-                      )}
-                    />
-
-                    <FormField
-                      control={form.control}
-                      name='completionRatio'
-                      render={({ field }) => (
-                        <FormItem>
-                          <FormLabel>{t('Completion ratio')}</FormLabel>
-                          <FormControl>
-                            <Input
-                              type='text'
-                              placeholder='1.0'
-                              {...field}
-                              onChange={(e) => {
-                                const value = e.target.value
-                                if (validateNumber(value)) {
-                                  field.onChange(value)
-                                  const ratio = form.getValues('ratio')
-                                  if (value && ratio) {
-                                    const compPrice =
-                                      parseFloat(ratio) * 2 * parseFloat(value)
-                                    setCompletionPrice(compPrice.toString())
-                                  } else {
-                                    setCompletionPrice('')
-                                  }
-                                }
-                              }}
-                            />
-                          </FormControl>
-                          <FormDescription>
-                            {field.value &&
-                            !isNaN(parseFloat(field.value)) &&
-                            promptPrice &&
-                            !isNaN(parseFloat(promptPrice))
-                              ? t(
-                                  'Calculated price: ${{price}} per 1M tokens',
-                                  {
-                                    price: (
-                                      parseFloat(promptPrice) *
-                                      parseFloat(field.value)
-                                    ).toFixed(4),
-                                  }
-                                )
-                              : t('Multiplier for completion tokens.')}
-                          </FormDescription>
-                          <FormMessage />
-                        </FormItem>
-                      )}
-                    />
-                  </>
-                ) : (
-                  <>
-                    <div className='space-y-4'>
-                      <div className='space-y-2'>
-                        <Label>{t('Prompt price ($/1M tokens)')}</Label>
-                        <Input
-                          type='text'
-                          placeholder='2.0'
-                          value={promptPrice}
-                          onChange={(e) =>
-                            handlePromptPriceChange(e.target.value)
-                          }
-                        />
-                        <p className='text-muted-foreground text-sm'>
-                          {promptPrice && !isNaN(parseFloat(promptPrice))
-                            ? t('Calculated ratio: {{ratio}}', {
-                                ratio: (parseFloat(promptPrice) / 2).toFixed(4),
-                              })
-                            : t('Enter Input price to calculate ratio')}
-                        </p>
-                      </div>
-
-                      <div className='space-y-2'>
-                        <Label>{t('Completion price ($/1M tokens)')}</Label>
-                        <Input
-                          type='text'
-                          placeholder='4.0'
-                          value={completionPrice}
-                          onChange={(e) =>
-                            handleCompletionPriceChange(e.target.value)
-                          }
-                        />
-                        <p className='text-muted-foreground text-sm'>
-                          {completionPrice &&
-                          !isNaN(parseFloat(completionPrice)) &&
-                          promptPrice &&
-                          !isNaN(parseFloat(promptPrice)) &&
-                          parseFloat(promptPrice) > 0
-                            ? t('Calculated ratio: {{ratio}}', {
-                                ratio: (
-                                  parseFloat(completionPrice) /
-                                  parseFloat(promptPrice)
-                                ).toFixed(4),
-                              })
-                            : t('Enter Completion price to calculate ratio')}
-                        </p>
-                      </div>
-                    </div>
-                  </>
-                )}
-
-                <Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
-                  <CollapsibleTrigger
-                    render={
-                      <Button
-                        type='button'
-                        variant='outline'
-                        className='flex w-full items-center justify-between'
-                      />
-                    }
-                  >
-                    {t('Advanced options')}
-                    <ChevronDown
-                      className={`h-4 w-4 transition-transform duration-200 ${
-                        advancedOpen ? 'rotate-180' : ''
-                      }`}
-                    />
-                  </CollapsibleTrigger>
-                  <CollapsibleContent className='space-y-6 pt-6'>
-                    <FormField
-                      control={form.control}
-                      name='cacheRatio'
-                      render={({ field }) => (
-                        <FormItem>
-                          <FormLabel>{t('Cache ratio')}</FormLabel>
-                          <FormControl>
-                            <Input
-                              type='text'
-                              placeholder='0.1'
-                              {...field}
-                              onChange={(e) => {
-                                const value = e.target.value
-                                if (validateNumber(value)) {
-                                  field.onChange(value)
-                                }
-                              }}
-                            />
-                          </FormControl>
-                          <FormDescription>
-                            {t('Discount ratio for cache hits.')}
-                          </FormDescription>
-                          <FormMessage />
-                        </FormItem>
-                      )}
-                    />
-
-                    <FormField
-                      control={form.control}
-                      name='createCacheRatio'
-                      render={({ field }) => (
-                        <FormItem>
-                          <FormLabel>{t('Create cache ratio')}</FormLabel>
-                          <FormControl>
-                            <Input
-                              type='text'
-                              placeholder='1.25'
-                              {...field}
-                              onChange={(e) => {
-                                const value = e.target.value
-                                if (validateNumber(value)) {
-                                  field.onChange(value)
-                                }
-                              }}
-                            />
-                          </FormControl>
-                          <FormDescription>
-                            {t(
-                              'Ratio applied when creating cache entries for supported models.'
-                            )}
-                          </FormDescription>
-                          <FormMessage />
-                        </FormItem>
-                      )}
-                    />
-
-                    <FormField
-                      control={form.control}
-                      name='imageRatio'
-                      render={({ field }) => (
-                        <FormItem>
-                          <FormLabel>{t('Image ratio')}</FormLabel>
-                          <FormControl>
-                            <Input
-                              type='text'
-                              placeholder='1.0'
-                              {...field}
-                              onChange={(e) => {
-                                const value = e.target.value
-                                if (validateNumber(value)) {
-                                  field.onChange(value)
-                                }
-                              }}
-                            />
-                          </FormControl>
-                          <FormDescription>
-                            {t('Multiplier for image processing.')}
-                          </FormDescription>
-                          <FormMessage />
-                        </FormItem>
-                      )}
-                    />
-
-                    <FormField
-                      control={form.control}
-                      name='audioRatio'
-                      render={({ field }) => (
-                        <FormItem>
-                          <FormLabel>{t('Audio ratio')}</FormLabel>
-                          <FormControl>
-                            <Input
-                              type='text'
-                              placeholder='1.0'
-                              {...field}
-                              onChange={(e) => {
-                                const value = e.target.value
-                                if (validateNumber(value)) {
-                                  field.onChange(value)
-                                }
-                              }}
-                            />
-                          </FormControl>
-                          <FormDescription>
-                            {t('Multiplier for audio inputs.')}
-                          </FormDescription>
-                          <FormMessage />
-                        </FormItem>
-                      )}
-                    />
-
-                    <FormField
-                      control={form.control}
-                      name='audioCompletionRatio'
-                      render={({ field }) => (
-                        <FormItem>
-                          <FormLabel>{t('Audio completion ratio')}</FormLabel>
-                          <FormControl>
-                            <Input
-                              type='text'
-                              placeholder='1.0'
-                              {...field}
-                              onChange={(e) => {
-                                const value = e.target.value
-                                if (validateNumber(value)) {
-                                  field.onChange(value)
-                                }
-                              }}
-                            />
-                          </FormControl>
-                          <FormDescription>
-                            {t('Multiplier for audio outputs.')}
-                          </FormDescription>
-                          <FormMessage />
-                        </FormItem>
-                      )}
-                    />
-                  </CollapsibleContent>
-                </Collapsible>
-              </>
-            )}
-
-            <DialogFooter>
-              <Button
-                type='button'
-                variant='outline'
-                onClick={() => onOpenChange(false)}
-              >
-                {t('Cancel')}
-              </Button>
-              <Button type='submit'>
-                {isEditMode ? t('Update') : t('Add')}
-              </Button>
-            </DialogFooter>
-          </form>
-        </Form>
-      </DialogContent>
-    </Dialog>
-  )
-}

+ 476 - 234
web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx

@@ -1,19 +1,27 @@
 import { useState, useMemo, memo, useCallback, useEffect } from 'react'
 import { useState, useMemo, memo, useCallback, useEffect } from 'react'
 import {
 import {
   type ColumnDef,
   type ColumnDef,
+  type ColumnFiltersState,
+  type OnChangeFn,
   type PaginationState,
   type PaginationState,
+  type RowSelectionState,
   type VisibilityState,
   type VisibilityState,
   type SortingState,
   type SortingState,
   flexRender,
   flexRender,
   getCoreRowModel,
   getCoreRowModel,
+  getFacetedRowModel,
+  getFacetedUniqueValues,
   getFilteredRowModel,
   getFilteredRowModel,
   getSortedRowModel,
   getSortedRowModel,
   getPaginationRowModel,
   getPaginationRowModel,
   useReactTable,
   useReactTable,
 } from '@tanstack/react-table'
 } from '@tanstack/react-table'
-import { Pencil, Plus, Trash2 } from 'lucide-react'
+import { useMediaQuery } from '@/hooks'
+import { Copy, Pencil, Plus, Trash2 } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
+import { toast } from 'sonner'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
+import { Checkbox } from '@/components/ui/checkbox'
 import {
 import {
   Table,
   Table,
   TableBody,
   TableBody,
@@ -23,6 +31,7 @@ import {
   TableRow,
   TableRow,
 } from '@/components/ui/table'
 } from '@/components/ui/table'
 import {
 import {
+  DataTableBulkActions,
   DataTableColumnHeader,
   DataTableColumnHeader,
   DataTableToolbar,
   DataTableToolbar,
   DataTablePagination,
   DataTablePagination,
@@ -33,7 +42,11 @@ import {
   splitBillingExprAndRequestRules,
   splitBillingExprAndRequestRules,
 } from '@/features/pricing/lib/billing-expr'
 } from '@/features/pricing/lib/billing-expr'
 import { safeJsonParse } from '../utils/json-parser'
 import { safeJsonParse } from '../utils/json-parser'
-import { ModelRatioDialog, type ModelRatioData } from './model-ratio-dialog'
+import {
+  ModelPricingEditorPanel,
+  ModelPricingSheet,
+  type ModelRatioData,
+} from './model-pricing-sheet'
 
 
 type ModelRatioVisualEditorProps = {
 type ModelRatioVisualEditorProps = {
   modelPrice: string
   modelPrice: string
@@ -67,9 +80,101 @@ type ModelRow = {
 
 
 const STORAGE_KEY = 'model-ratio-column-visibility'
 const STORAGE_KEY = 'model-ratio-column-visibility'
 
 
-const formatValue = (value?: string) => {
-  if (!value || value === '') return '—'
-  return value
+const hasValue = (value?: string) => value !== undefined && value !== ''
+
+const toNumberOrNull = (value?: string) => {
+  if (!hasValue(value)) return null
+  const num = Number(value)
+  return Number.isFinite(num) ? num : null
+}
+
+const formatPrice = (value: number) => {
+  return Number.parseFloat(value.toFixed(12)).toString()
+}
+
+const ratioToPrice = (ratio?: string, denominator?: string) => {
+  const ratioNumber = toNumberOrNull(ratio)
+  const denominatorNumber = denominator ? toNumberOrNull(denominator) : 2
+  if (ratioNumber === null || denominatorNumber === null) return ''
+  return formatPrice(ratioNumber * denominatorNumber)
+}
+
+const filterBySelectedValues = (
+  rowValue: unknown,
+  filterValue: unknown
+): boolean => {
+  if (!Array.isArray(filterValue) || filterValue.length === 0) return true
+  return filterValue.includes(String(rowValue))
+}
+
+const getModeLabel = (mode?: string) => {
+  if (mode === 'per-request') return 'Per-request'
+  if (mode === 'tiered_expr') return 'Expression'
+  return 'Per-token'
+}
+
+const getModeVariant = (mode?: string): 'warning' | 'info' | 'success' => {
+  if (mode === 'per-request') return 'warning'
+  if (mode === 'tiered_expr') return 'info'
+  return 'success'
+}
+
+const getExpressionSummary = (row: ModelRow, t: (key: string) => string) => {
+  const tierCount = (row.billingExpr?.match(/tier\(/g) || []).length
+  if (tierCount > 0) {
+    return `${t('Tiered pricing')} · ${tierCount} ${t('tiers')}`
+  }
+  return t('Expression pricing')
+}
+
+const getPriceSummary = (row: ModelRow, t: (key: string) => string) => {
+  if (row.billingMode === 'tiered_expr') {
+    return getExpressionSummary(row, t)
+  }
+  if (row.billingMode === 'per-request') {
+    return row.price ? `$${row.price} / ${t('request')}` : t('Unset price')
+  }
+
+  const inputPrice = ratioToPrice(row.ratio)
+  if (!inputPrice) return t('Unset price')
+
+  const extraCount = [
+    row.completionRatio,
+    row.cacheRatio,
+    row.createCacheRatio,
+    row.imageRatio,
+    row.audioRatio,
+    row.audioCompletionRatio,
+  ].filter(hasValue).length
+
+  return extraCount > 0
+    ? `${t('Input')} $${inputPrice} · ${extraCount} ${t('extras')}`
+    : `${t('Input')} $${inputPrice}`
+}
+
+const getPriceDetail = (row: ModelRow, t: (key: string) => string) => {
+  if (row.billingMode === 'tiered_expr') {
+    return row.requestRuleExpr
+      ? t('Includes request rules')
+      : t('Expression based')
+  }
+  if (row.billingMode === 'per-request') {
+    return t('Fixed request price')
+  }
+
+  const inputPrice = ratioToPrice(row.ratio)
+  if (!inputPrice) return t('No base input price')
+
+  const details = [
+    row.completionRatio &&
+      `${t('Output')} $${ratioToPrice(row.completionRatio, inputPrice)}`,
+    row.cacheRatio &&
+      `${t('Cache')} $${ratioToPrice(row.cacheRatio, inputPrice)}`,
+    row.createCacheRatio &&
+      `${t('Cache write')} $${ratioToPrice(row.createCacheRatio, inputPrice)}`,
+  ].filter(Boolean)
+
+  return details.length > 0 ? details.join(' · ') : t('Base input price only')
 }
 }
 
 
 export const ModelRatioVisualEditor = memo(
 export const ModelRatioVisualEditor = memo(
@@ -87,12 +192,17 @@ export const ModelRatioVisualEditor = memo(
     onChange,
     onChange,
   }: ModelRatioVisualEditorProps) {
   }: ModelRatioVisualEditorProps) {
     const { t } = useTranslation()
     const { t } = useTranslation()
-    const [dialogOpen, setDialogOpen] = useState(false)
+    const isMobile = useMediaQuery('(max-width: 767px)')
+    const [sheetOpen, setSheetOpen] = useState(false)
+    const [editorOpen, setEditorOpen] = useState(false)
     const [editData, setEditData] = useState<ModelRatioData | null>(null)
     const [editData, setEditData] = useState<ModelRatioData | null>(null)
     const [sorting, setSorting] = useState<SortingState>([])
     const [sorting, setSorting] = useState<SortingState>([])
+    const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
+    const [globalFilter, setGlobalFilter] = useState('')
+    const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
     const [pagination, setPagination] = useState<PaginationState>({
     const [pagination, setPagination] = useState<PaginationState>({
       pageIndex: 0,
       pageIndex: 0,
-      pageSize: 10,
+      pageSize: 20,
     })
     })
     const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
     const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
       () => {
       () => {
@@ -102,6 +212,7 @@ export const ModelRatioVisualEditor = memo(
             return safeJsonParse<VisibilityState>(saved, {
             return safeJsonParse<VisibilityState>(saved, {
               fallback: {
               fallback: {
                 cacheRatio: false,
                 cacheRatio: false,
+                createCacheRatio: false,
                 imageRatio: false,
                 imageRatio: false,
                 audioRatio: false,
                 audioRatio: false,
                 audioCompletionRatio: false,
                 audioCompletionRatio: false,
@@ -265,34 +376,82 @@ export const ModelRatioVisualEditor = memo(
       billingExpr,
       billingExpr,
     ])
     ])
 
 
-    const handleEdit = useCallback((model: ModelRow) => {
-      setEditData({
-        name: model.name,
-        price: model.price,
-        ratio: model.ratio,
-        cacheRatio: model.cacheRatio,
-        createCacheRatio: model.createCacheRatio,
-        completionRatio: model.completionRatio,
-        imageRatio: model.imageRatio,
-        audioRatio: model.audioRatio,
-        audioCompletionRatio: model.audioCompletionRatio,
-        billingMode:
-          model.billingMode === 'tiered_expr'
-            ? 'tiered_expr'
-            : model.price && model.price !== ''
-              ? 'per-request'
-              : 'per-token',
-        billingExpr: model.billingExpr,
-        requestRuleExpr: model.requestRuleExpr,
-      })
-      setDialogOpen(true)
-    }, [])
+    const modeCounts = useMemo(
+      () =>
+        models.reduce(
+          (acc, model) => {
+            const mode =
+              model.billingMode === 'per-request' ||
+              model.billingMode === 'tiered_expr'
+                ? model.billingMode
+                : 'per-token'
+            acc[mode] += 1
+            return acc
+          },
+          {
+            'per-token': 0,
+            'per-request': 0,
+            tiered_expr: 0,
+          } as Record<'per-token' | 'per-request' | 'tiered_expr', number>
+        ),
+      [models]
+    )
+
+    const handleEdit = useCallback(
+      (model: ModelRow) => {
+        setEditData({
+          name: model.name,
+          price: model.price,
+          ratio: model.ratio,
+          cacheRatio: model.cacheRatio,
+          createCacheRatio: model.createCacheRatio,
+          completionRatio: model.completionRatio,
+          imageRatio: model.imageRatio,
+          audioRatio: model.audioRatio,
+          audioCompletionRatio: model.audioCompletionRatio,
+          billingMode:
+            model.billingMode === 'tiered_expr'
+              ? 'tiered_expr'
+              : model.price && model.price !== ''
+                ? 'per-request'
+                : 'per-token',
+          billingExpr: model.billingExpr,
+          requestRuleExpr: model.requestRuleExpr,
+        })
+        setEditorOpen(true)
+        if (isMobile) setSheetOpen(true)
+      },
+      [isMobile]
+    )
 
 
     const handleAdd = useCallback(() => {
     const handleAdd = useCallback(() => {
       setEditData(null)
       setEditData(null)
-      setDialogOpen(true)
+      setEditorOpen(true)
+      if (isMobile) setSheetOpen(true)
+    }, [isMobile])
+
+    const handleCancel = useCallback(() => {
+      setEditData(null)
+      setEditorOpen(false)
+      setSheetOpen(false)
     }, [])
     }, [])
 
 
+    const handleGlobalFilterChange = useCallback<OnChangeFn<string>>(
+      (updater) => {
+        setGlobalFilter((previous) => {
+          const next =
+            typeof updater === 'function' ? updater(previous) : updater
+          if (next !== previous) {
+            setEditData(null)
+            setEditorOpen(false)
+            setSheetOpen(false)
+          }
+          return next
+        })
+      },
+      []
+    )
+
     const handleDelete = useCallback(
     const handleDelete = useCallback(
       (name: string) => {
       (name: string) => {
         const priceMap = safeJsonParse<Record<string, number>>(modelPrice, {
         const priceMap = safeJsonParse<Record<string, number>>(modelPrice, {
@@ -383,15 +542,32 @@ export const ModelRatioVisualEditor = memo(
     )
     )
 
 
     const columns = useMemo<ColumnDef<ModelRow>[]>(() => {
     const columns = useMemo<ColumnDef<ModelRow>[]>(() => {
-      // Ratio fields are not the primary pricing when a per-request fixed
-      // price is set, or when the model is in tiered_expr mode (the
-      // expression is primary; ratios are fallback during sync delays).
-      const isFallbackRow = (row: ModelRow) =>
-        row.billingMode === 'tiered_expr' || !!row.price
-      const fallbackClass = (row: ModelRow) =>
-        isFallbackRow(row) ? 'text-muted-foreground' : ''
-
       return [
       return [
+        {
+          id: 'select',
+          header: ({ table }) => (
+            <Checkbox
+              checked={table.getIsAllPageRowsSelected()}
+              indeterminate={table.getIsSomePageRowsSelected()}
+              onCheckedChange={(value) =>
+                table.toggleAllPageRowsSelected(!!value)
+              }
+              aria-label={t('Select all')}
+              className='translate-y-[2px]'
+            />
+          ),
+          cell: ({ row }) => (
+            <Checkbox
+              checked={row.getIsSelected()}
+              onCheckedChange={(value) => row.toggleSelected(!!value)}
+              aria-label={t('Select row')}
+              className='translate-y-[2px]'
+            />
+          ),
+          enableSorting: false,
+          enableHiding: false,
+          meta: { label: t('Select') },
+        },
         {
         {
           accessorKey: 'name',
           accessorKey: 'name',
           header: ({ column }) => (
           header: ({ column }) => (
@@ -419,106 +595,41 @@ export const ModelRatioVisualEditor = memo(
           enableHiding: false,
           enableHiding: false,
         },
         },
         {
         {
-          accessorKey: 'price',
-          header: ({ column }) => (
-            <DataTableColumnHeader column={column} title={t('Fixed price')} />
-          ),
-          cell: ({ row }) => (
-            <span
-              className={
-                row.original.billingMode === 'tiered_expr'
-                  ? 'text-muted-foreground'
-                  : ''
-              }
-            >
-              {formatValue(row.getValue('price'))}
-            </span>
-          ),
-          meta: { label: 'Fixed price' },
-        },
-        {
-          accessorKey: 'ratio',
-          header: ({ column }) => (
-            <DataTableColumnHeader column={column} title={t('Ratio')} />
-          ),
-          cell: ({ row }) => (
-            <span className={fallbackClass(row.original)}>
-              {formatValue(row.getValue('ratio'))}
-            </span>
-          ),
-          meta: { label: 'Ratio' },
-        },
-        {
-          accessorKey: 'completionRatio',
-          header: ({ column }) => (
-            <DataTableColumnHeader column={column} title={t('Completion')} />
-          ),
-          cell: ({ row }) => (
-            <span className={fallbackClass(row.original)}>
-              {formatValue(row.getValue('completionRatio'))}
-            </span>
-          ),
-          meta: { label: 'Completion' },
-        },
-        {
-          accessorKey: 'cacheRatio',
-          header: ({ column }) => (
-            <DataTableColumnHeader column={column} title={t('Cache')} />
-          ),
-          cell: ({ row }) => (
-            <span className={fallbackClass(row.original)}>
-              {formatValue(row.getValue('cacheRatio'))}
-            </span>
-          ),
-          meta: { label: 'Cache' },
-        },
-        {
-          accessorKey: 'createCacheRatio',
-          header: ({ column }) => (
-            <DataTableColumnHeader column={column} title={t('Create cache')} />
-          ),
-          cell: ({ row }) => (
-            <span className={fallbackClass(row.original)}>
-              {formatValue(row.getValue('createCacheRatio'))}
-            </span>
-          ),
-          meta: { label: 'Create cache' },
-        },
-        {
-          accessorKey: 'imageRatio',
-          header: ({ column }) => (
-            <DataTableColumnHeader column={column} title={t('Image')} />
-          ),
-          cell: ({ row }) => (
-            <span className={fallbackClass(row.original)}>
-              {formatValue(row.getValue('imageRatio'))}
-            </span>
-          ),
-          meta: { label: 'Image' },
-        },
-        {
-          accessorKey: 'audioRatio',
+          accessorKey: 'billingMode',
           header: ({ column }) => (
           header: ({ column }) => (
-            <DataTableColumnHeader column={column} title={t('Audio')} />
+            <DataTableColumnHeader column={column} title={t('Mode')} />
           ),
           ),
           cell: ({ row }) => (
           cell: ({ row }) => (
-            <span className={fallbackClass(row.original)}>
-              {formatValue(row.getValue('audioRatio'))}
-            </span>
+            <StatusBadge
+              label={t(getModeLabel(row.original.billingMode))}
+              variant={getModeVariant(row.original.billingMode)}
+              copyable={false}
+            />
           ),
           ),
-          meta: { label: 'Audio' },
+          filterFn: (row, id, value) =>
+            filterBySelectedValues(row.getValue(id), value),
+          meta: { label: t('Mode') },
         },
         },
         {
         {
-          accessorKey: 'audioCompletionRatio',
+          id: 'priceSummary',
           header: ({ column }) => (
           header: ({ column }) => (
-            <DataTableColumnHeader column={column} title={t('Audio comp.')} />
+            <DataTableColumnHeader column={column} title={t('Price summary')} />
           ),
           ),
           cell: ({ row }) => (
           cell: ({ row }) => (
-            <span className={fallbackClass(row.original)}>
-              {formatValue(row.getValue('audioCompletionRatio'))}
-            </span>
+            <div className='flex min-w-[180px] flex-col gap-1'>
+              <span className='font-medium'>
+                {getPriceSummary(row.original, t)}
+              </span>
+              <span className='text-muted-foreground max-w-[320px] truncate text-xs'>
+                {getPriceDetail(row.original, t)}
+              </span>
+            </div>
           ),
           ),
-          meta: { label: 'Audio comp.' },
+          sortingFn: (rowA, rowB) =>
+            getPriceSummary(rowA.original, t).localeCompare(
+              getPriceSummary(rowB.original, t)
+            ),
+          meta: { label: t('Price summary') },
         },
         },
         {
         {
           id: 'actions',
           id: 'actions',
@@ -529,14 +640,14 @@ export const ModelRatioVisualEditor = memo(
                 size='sm'
                 size='sm'
                 onClick={() => handleEdit(row.original)}
                 onClick={() => handleEdit(row.original)}
               >
               >
-                <Pencil className='h-4 w-4' />
+                <Pencil />
               </Button>
               </Button>
               <Button
               <Button
                 variant='ghost'
                 variant='ghost'
                 size='sm'
                 size='sm'
                 onClick={() => handleDelete(row.original.name)}
                 onClick={() => handleDelete(row.original.name)}
               >
               >
-                <Trash2 className='h-4 w-4' />
+                <Trash2 />
               </Button>
               </Button>
             </div>
             </div>
           ),
           ),
@@ -550,25 +661,34 @@ export const ModelRatioVisualEditor = memo(
       columns,
       columns,
       state: {
       state: {
         sorting,
         sorting,
+        columnFilters,
+        globalFilter,
         columnVisibility,
         columnVisibility,
         pagination,
         pagination,
+        rowSelection,
       },
       },
+      enableRowSelection: true,
       onSortingChange: setSorting,
       onSortingChange: setSorting,
+      onColumnFiltersChange: setColumnFilters,
+      onGlobalFilterChange: handleGlobalFilterChange,
       onColumnVisibilityChange: setColumnVisibility,
       onColumnVisibilityChange: setColumnVisibility,
       onPaginationChange: setPagination,
       onPaginationChange: setPagination,
+      onRowSelectionChange: setRowSelection,
       autoResetPageIndex: false,
       autoResetPageIndex: false,
       getCoreRowModel: getCoreRowModel(),
       getCoreRowModel: getCoreRowModel(),
       getFilteredRowModel: getFilteredRowModel(),
       getFilteredRowModel: getFilteredRowModel(),
       getSortedRowModel: getSortedRowModel(),
       getSortedRowModel: getSortedRowModel(),
       getPaginationRowModel: getPaginationRowModel(),
       getPaginationRowModel: getPaginationRowModel(),
+      getFacetedRowModel: getFacetedRowModel(),
+      getFacetedUniqueValues: getFacetedUniqueValues(),
       globalFilterFn: (row, _columnId, filterValue) => {
       globalFilterFn: (row, _columnId, filterValue) => {
         const searchValue = String(filterValue).toLowerCase()
         const searchValue = String(filterValue).toLowerCase()
         return row.original.name.toLowerCase().includes(searchValue)
         return row.original.name.toLowerCase().includes(searchValue)
       },
       },
     })
     })
 
 
-    const handleSave = useCallback(
-      (data: ModelRatioData) => {
+    const persistPricingData = useCallback(
+      (data: ModelRatioData, targetNames: string[] = [data.name]) => {
         const priceMap = safeJsonParse<Record<string, number>>(modelPrice, {
         const priceMap = safeJsonParse<Record<string, number>>(modelPrice, {
           fallback: {},
           fallback: {},
           silent: true,
           silent: true,
@@ -610,58 +730,61 @@ export const ModelRatioVisualEditor = memo(
           { fallback: {}, silent: true }
           { fallback: {}, silent: true }
         )
         )
 
 
-        delete priceMap[data.name]
-        delete ratioMap[data.name]
-        delete cacheMap[data.name]
-        delete createCacheMap[data.name]
-        delete completionMap[data.name]
-        delete imageMap[data.name]
-        delete audioMap[data.name]
-        delete audioCompletionMap[data.name]
-        delete billingModeMap[data.name]
-        delete billingExprMap[data.name]
-
         const setIfPresent = (
         const setIfPresent = (
           target: Record<string, number>,
           target: Record<string, number>,
+          name: string,
           value: string | undefined
           value: string | undefined
         ) => {
         ) => {
           if (!value || value === '') return
           if (!value || value === '') return
           const parsed = parseFloat(value)
           const parsed = parseFloat(value)
-          if (Number.isFinite(parsed)) target[data.name] = parsed
+          if (Number.isFinite(parsed)) target[name] = parsed
         }
         }
 
 
-        if (data.billingMode === 'tiered_expr') {
-          const combined = combineBillingExpr(
-            data.billingExpr || '',
-            data.requestRuleExpr || ''
-          )
-          if (combined) {
-            billingModeMap[data.name] = 'tiered_expr'
-            billingExprMap[data.name] = combined
+        targetNames.forEach((name) => {
+          delete priceMap[name]
+          delete ratioMap[name]
+          delete cacheMap[name]
+          delete createCacheMap[name]
+          delete completionMap[name]
+          delete imageMap[name]
+          delete audioMap[name]
+          delete audioCompletionMap[name]
+          delete billingModeMap[name]
+          delete billingExprMap[name]
+
+          if (data.billingMode === 'tiered_expr') {
+            const combined = combineBillingExpr(
+              data.billingExpr || '',
+              data.requestRuleExpr || ''
+            )
+            if (combined) {
+              billingModeMap[name] = 'tiered_expr'
+              billingExprMap[name] = combined
+            }
+            // Always serialize ratio/price values for tiered_expr models so they
+            // serve as fallback during multi-instance sync delays. The backend's
+            // ModelPriceHelper checks billing_mode first, so these values are
+            // only consulted when billing_setting hasn't propagated yet.
+            setIfPresent(priceMap, name, data.price)
+            setIfPresent(ratioMap, name, data.ratio)
+            setIfPresent(cacheMap, name, data.cacheRatio)
+            setIfPresent(createCacheMap, name, data.createCacheRatio)
+            setIfPresent(completionMap, name, data.completionRatio)
+            setIfPresent(imageMap, name, data.imageRatio)
+            setIfPresent(audioMap, name, data.audioRatio)
+            setIfPresent(audioCompletionMap, name, data.audioCompletionRatio)
+          } else if (data.price && data.price !== '') {
+            setIfPresent(priceMap, name, data.price)
+          } else {
+            setIfPresent(ratioMap, name, data.ratio)
+            setIfPresent(cacheMap, name, data.cacheRatio)
+            setIfPresent(createCacheMap, name, data.createCacheRatio)
+            setIfPresent(completionMap, name, data.completionRatio)
+            setIfPresent(imageMap, name, data.imageRatio)
+            setIfPresent(audioMap, name, data.audioRatio)
+            setIfPresent(audioCompletionMap, name, data.audioCompletionRatio)
           }
           }
-          // Always serialize ratio/price values for tiered_expr models so they
-          // serve as fallback during multi-instance sync delays. The backend's
-          // ModelPriceHelper checks billing_mode first, so these values are
-          // only consulted when billing_setting hasn't propagated yet.
-          setIfPresent(priceMap, data.price)
-          setIfPresent(ratioMap, data.ratio)
-          setIfPresent(cacheMap, data.cacheRatio)
-          setIfPresent(createCacheMap, data.createCacheRatio)
-          setIfPresent(completionMap, data.completionRatio)
-          setIfPresent(imageMap, data.imageRatio)
-          setIfPresent(audioMap, data.audioRatio)
-          setIfPresent(audioCompletionMap, data.audioCompletionRatio)
-        } else if (data.price && data.price !== '') {
-          setIfPresent(priceMap, data.price)
-        } else {
-          setIfPresent(ratioMap, data.ratio)
-          setIfPresent(cacheMap, data.cacheRatio)
-          setIfPresent(createCacheMap, data.createCacheRatio)
-          setIfPresent(completionMap, data.completionRatio)
-          setIfPresent(imageMap, data.imageRatio)
-          setIfPresent(audioMap, data.audioRatio)
-          setIfPresent(audioCompletionMap, data.audioCompletionRatio)
-        }
+        })
 
 
         onChange('ModelPrice', JSON.stringify(priceMap, null, 2))
         onChange('ModelPrice', JSON.stringify(priceMap, null, 2))
         onChange('ModelRatio', JSON.stringify(ratioMap, null, 2))
         onChange('ModelRatio', JSON.stringify(ratioMap, null, 2))
@@ -698,72 +821,191 @@ export const ModelRatioVisualEditor = memo(
       ]
       ]
     )
     )
 
 
+    const handleSave = useCallback(
+      (data: ModelRatioData) => {
+        persistPricingData(data)
+        setEditData(data)
+        setEditorOpen(true)
+      },
+      [persistPricingData]
+    )
+
+    const handleBatchCopy = useCallback(() => {
+      if (!editData) {
+        toast.error(t('Open a source model first'))
+        return
+      }
+
+      const targetNames = table
+        .getFilteredSelectedRowModel()
+        .rows.map((row) => row.original.name)
+
+      if (targetNames.length === 0) {
+        toast.error(t('Select at least one target model'))
+        return
+      }
+
+      persistPricingData(editData, targetNames)
+      table.resetRowSelection()
+      toast.success(
+        t('Applied {{name}} pricing to {{count}} models', {
+          name: editData.name,
+          count: targetNames.length,
+        })
+      )
+    }, [editData, persistPricingData, t, table])
+
+    const selectedTargetCount = table.getFilteredSelectedRowModel().rows.length
+
     return (
     return (
-      <div className='space-y-4'>
-        <div className='flex items-center justify-between gap-4'>
-          <DataTableToolbar
-            table={table}
-            searchPlaceholder={t('Search models...')}
-          />
-          <Button onClick={handleAdd}>
-            <Plus className='mr-2 h-4 w-4' />
-            {t('Add model')}
-          </Button>
-        </div>
+      <div className='flex flex-col gap-4'>
+        <div className='grid min-h-0 gap-4 md:grid-cols-[minmax(0,1fr)_minmax(420px,0.82fr)] xl:grid-cols-[minmax(0,1.1fr)_minmax(520px,0.9fr)]'>
+          <div className='flex min-w-0 flex-col gap-4'>
+            <DataTableToolbar
+              table={table}
+              searchPlaceholder={t('Search models...')}
+              filters={[
+                {
+                  columnId: 'billingMode',
+                  title: t('Mode'),
+                  options: [
+                    {
+                      label: 'Per-token',
+                      value: 'per-token',
+                      count: modeCounts['per-token'],
+                    },
+                    {
+                      label: 'Per-request',
+                      value: 'per-request',
+                      count: modeCounts['per-request'],
+                    },
+                    {
+                      label: 'Expression',
+                      value: 'tiered_expr',
+                      count: modeCounts.tiered_expr,
+                    },
+                  ],
+                },
+              ]}
+              preActions={
+                <Button onClick={handleAdd}>
+                  <Plus data-icon='inline-start' />
+                  {t('Add model')}
+                </Button>
+              }
+            />
 
 
-        {table.getRowModel().rows.length === 0 ? (
-          <div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
-            {table.getState().globalFilter
-              ? t('No models match your search')
-              : t('No models configured. Click "Add model" to get started.')}
-          </div>
-        ) : (
-          <div className='overflow-hidden rounded-md border'>
-            <Table>
-              <TableHeader>
-                {table.getHeaderGroups().map((headerGroup) => (
-                  <TableRow key={headerGroup.id}>
-                    {headerGroup.headers.map((header) => (
-                      <TableHead key={header.id} colSpan={header.colSpan}>
-                        {header.isPlaceholder
-                          ? null
-                          : flexRender(
-                              header.column.columnDef.header,
-                              header.getContext()
-                            )}
-                      </TableHead>
+            {table.getRowModel().rows.length === 0 ? (
+              <div className='text-muted-foreground rounded-lg border border-dashed p-8 text-center'>
+                {table.getState().globalFilter
+                  ? t('No models match your search')
+                  : t('No models configured. Use Add model to get started.')}
+              </div>
+            ) : (
+              <div className='overflow-hidden rounded-md border'>
+                <Table>
+                  <TableHeader>
+                    {table.getHeaderGroups().map((headerGroup) => (
+                      <TableRow key={headerGroup.id}>
+                        {headerGroup.headers.map((header) => (
+                          <TableHead key={header.id} colSpan={header.colSpan}>
+                            {header.isPlaceholder
+                              ? null
+                              : flexRender(
+                                  header.column.columnDef.header,
+                                  header.getContext()
+                                )}
+                          </TableHead>
+                        ))}
+                      </TableRow>
                     ))}
                     ))}
-                  </TableRow>
-                ))}
-              </TableHeader>
-              <TableBody>
-                {table.getRowModel().rows.map((row) => (
-                  <TableRow key={row.id}>
-                    {row.getVisibleCells().map((cell) => (
-                      <TableCell key={cell.id}>
-                        {flexRender(
-                          cell.column.columnDef.cell,
-                          cell.getContext()
-                        )}
-                      </TableCell>
+                  </TableHeader>
+                  <TableBody>
+                    {table.getRowModel().rows.map((row) => (
+                      <TableRow
+                        key={row.id}
+                        data-state={
+                          row.getIsSelected() ? 'selected' : undefined
+                        }
+                        className={
+                          editData?.name === row.original.name
+                            ? 'bg-muted/45'
+                            : undefined
+                        }
+                        onClick={(event) => {
+                          const target = event.target as HTMLElement
+                          if (target.closest('button, [role="checkbox"]'))
+                            return
+                          handleEdit(row.original)
+                        }}
+                      >
+                        {row.getVisibleCells().map((cell) => (
+                          <TableCell key={cell.id}>
+                            {flexRender(
+                              cell.column.columnDef.cell,
+                              cell.getContext()
+                            )}
+                          </TableCell>
+                        ))}
+                      </TableRow>
                     ))}
                     ))}
-                  </TableRow>
-                ))}
-              </TableBody>
-            </Table>
+                  </TableBody>
+                </Table>
+              </div>
+            )}
+
+            {table.getRowModel().rows.length > 0 && (
+              <DataTablePagination table={table} />
+            )}
           </div>
           </div>
-        )}
 
 
-        {table.getRowModel().rows.length > 0 && (
-          <DataTablePagination table={table} />
-        )}
+          <div className='hidden min-w-0 md:block'>
+            {editorOpen ? (
+              <ModelPricingEditorPanel
+                onSave={handleSave}
+                onCancel={handleCancel}
+                editData={editData}
+                selectedTargetCount={selectedTargetCount}
+                className='sticky top-4 h-[calc(100vh-8rem)] min-h-[620px]'
+              />
+            ) : (
+              <div className='bg-card text-muted-foreground sticky top-4 flex h-[calc(100vh-8rem)] min-h-[420px] flex-col items-center justify-center gap-3 rounded-xl border border-dashed p-6 text-center'>
+                <div className='text-foreground text-base font-medium'>
+                  {t('Select a model to edit pricing')}
+                </div>
+                <p className='max-w-sm text-sm'>
+                  {t(
+                    'Use the full-width table to scan prices, then select a row to edit it here.'
+                  )}
+                </p>
+                <Button variant='outline' onClick={handleAdd}>
+                  <Plus data-icon='inline-start' />
+                  {t('Add model')}
+                </Button>
+              </div>
+            )}
+          </div>
+        </div>
 
 
-        <ModelRatioDialog
-          open={dialogOpen}
-          onOpenChange={setDialogOpen}
-          onSave={handleSave}
-          editData={editData}
-        />
+        <DataTableBulkActions table={table} entityName={t('model')}>
+          <Button size='sm' disabled={!editData} onClick={handleBatchCopy}>
+            <Copy data-icon='inline-start' />
+            {editData
+              ? t('Copy {{name}} pricing', { name: editData.name })
+              : t('Open a source model first')}
+          </Button>
+        </DataTableBulkActions>
+
+        {isMobile && (
+          <ModelPricingSheet
+            open={sheetOpen}
+            onOpenChange={setSheetOpen}
+            onSave={handleSave}
+            onCancel={handleCancel}
+            editData={editData}
+            selectedTargetCount={selectedTargetCount}
+          />
+        )}
       </div>
       </div>
     )
     )
   },
   },

+ 77 - 52
web/default/src/features/system-settings/models/ratio-settings-card.tsx

@@ -179,17 +179,24 @@ const groupSchema = z.object({
 
 
 type ModelFormValues = z.infer<typeof modelSchema>
 type ModelFormValues = z.infer<typeof modelSchema>
 type GroupFormValues = z.infer<typeof groupSchema>
 type GroupFormValues = z.infer<typeof groupSchema>
+type RatioTabId = 'models' | 'groups' | 'tool-prices' | 'upstream-sync'
 
 
 type RatioSettingsCardProps = {
 type RatioSettingsCardProps = {
   modelDefaults: ModelFormValues
   modelDefaults: ModelFormValues
   groupDefaults: GroupFormValues
   groupDefaults: GroupFormValues
   toolPricesDefault: string
   toolPricesDefault: string
+  titleKey?: string
+  descriptionKey?: string
+  visibleTabs?: RatioTabId[]
 }
 }
 
 
 export function RatioSettingsCard({
 export function RatioSettingsCard({
   modelDefaults,
   modelDefaults,
   groupDefaults,
   groupDefaults,
   toolPricesDefault,
   toolPricesDefault,
+  titleKey = 'Pricing Ratios',
+  descriptionKey = 'Configure model, caching, and group ratios used for billing',
+  visibleTabs = ['models', 'groups', 'tool-prices', 'upstream-sync'],
 }: RatioSettingsCardProps) {
 }: RatioSettingsCardProps) {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const updateOption = useUpdateOption()
   const updateOption = useUpdateOption()
@@ -414,61 +421,79 @@ export function RatioSettingsCard({
     resetMutate()
     resetMutate()
   }, [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>
       </Tabs>
 
 
       <ConfirmDialog
       <ConfirmDialog

+ 29 - 27
web/default/src/features/system-settings/models/section-registry.tsx

@@ -4,7 +4,8 @@ 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 { 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) {
 function formatJsonForEditor(value: string, fallback: string) {
   const raw = (value ?? '').toString().trim()
   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) => (
     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'],
         }}
         }}
       />
       />
     ),
     ),

+ 245 - 149
web/default/src/features/system-settings/models/tiered-pricing-editor.tsx

@@ -10,7 +10,7 @@ import {
   type InputHTMLAttributes,
   type InputHTMLAttributes,
   type MouseEvent as ReactMouseEvent,
   type MouseEvent as ReactMouseEvent,
 } from 'react'
 } from 'react'
-import { Copy, Plus, Trash2 } from 'lucide-react'
+import { ChevronDown, Copy, Plus, Trash2 } from 'lucide-react'
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
 import { toast } from 'sonner'
 import { toast } from 'sonner'
 import { cn } from '@/lib/utils'
 import { cn } from '@/lib/utils'
@@ -31,6 +31,7 @@ import {
   SelectTrigger,
   SelectTrigger,
   SelectValue,
   SelectValue,
 } from '@/components/ui/select'
 } from '@/components/ui/select'
+import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
 import { Textarea } from '@/components/ui/textarea'
 import { Textarea } from '@/components/ui/textarea'
 import {
 import {
   BILLING_EXTRA_VARS,
   BILLING_EXTRA_VARS,
@@ -38,6 +39,10 @@ import {
   MATCH_CONTAINS,
   MATCH_CONTAINS,
   MATCH_EQ,
   MATCH_EQ,
   MATCH_EXISTS,
   MATCH_EXISTS,
+  MATCH_GT,
+  MATCH_GTE,
+  MATCH_LT,
+  MATCH_LTE,
   MATCH_RANGE,
   MATCH_RANGE,
   SOURCE_HEADER,
   SOURCE_HEADER,
   SOURCE_PARAM,
   SOURCE_PARAM,
@@ -48,7 +53,6 @@ import {
   createEmptyCondition,
   createEmptyCondition,
   createEmptyRuleGroup,
   createEmptyRuleGroup,
   createEmptyTimeCondition,
   createEmptyTimeCondition,
-  createEmptyTimeRuleGroup,
   getRequestRuleMatchOptions,
   getRequestRuleMatchOptions,
   splitBillingExprAndRequestRules,
   splitBillingExprAndRequestRules,
   tryParseRequestRuleExpr,
   tryParseRequestRuleExpr,
@@ -77,11 +81,20 @@ import {
 } from '@/features/pricing/lib/tier-expr'
 } from '@/features/pricing/lib/tier-expr'
 
 
 const PRICE_SUFFIX = '$/1M tokens'
 const PRICE_SUFFIX = '$/1M tokens'
-
-const VAR_OPTIONS: { value: TierConditionInput['var']; label: string }[] = [
-  { value: 'len', label: 'len (input length)' },
-  { value: 'p', label: 'p (input)' },
-  { value: 'c', label: 'c (output)' },
+const CACHE_PRICE_VARS = BILLING_EXTRA_VARS.filter(
+  (variable) => variable.group === 'cache'
+)
+const MEDIA_PRICE_VARS = BILLING_EXTRA_VARS.filter(
+  (variable) => variable.group === 'media'
+)
+
+const CONDITION_INPUT_OPTIONS: {
+  value: TierConditionInput['var']
+  labelKey: string
+}[] = [
+  { value: 'len', labelKey: 'Full input length' },
+  { value: 'p', labelKey: 'Billable input tokens' },
+  { value: 'c', labelKey: 'Billable output tokens' },
 ]
 ]
 const OPS: TierConditionInput['op'][] = ['<', '<=', '>', '>=']
 const OPS: TierConditionInput['op'][] = ['<', '<=', '>', '>=']
 
 
@@ -394,6 +407,11 @@ type ConditionRowProps = {
 }
 }
 
 
 function ConditionRow({ condition, onChange, onRemove }: ConditionRowProps) {
 function ConditionRow({ condition, onChange, onRemove }: ConditionRowProps) {
+  const { t } = useTranslation()
+  const currentInputOption = CONDITION_INPUT_OPTIONS.find(
+    (option) => option.value === condition.var
+  )
+
   return (
   return (
     <div className='flex items-center gap-2'>
     <div className='flex items-center gap-2'>
       <Select
       <Select
@@ -403,12 +421,16 @@ function ConditionRow({ condition, onChange, onRemove }: ConditionRowProps) {
         }
         }
       >
       >
         <SelectTrigger className='w-32' size='sm'>
         <SelectTrigger className='w-32' size='sm'>
-          <SelectValue />
+          <SelectValue>
+            {currentInputOption
+              ? t(currentInputOption.labelKey)
+              : condition.var}
+          </SelectValue>
         </SelectTrigger>
         </SelectTrigger>
         <SelectContent>
         <SelectContent>
-          {VAR_OPTIONS.map((option) => (
+          {CONDITION_INPUT_OPTIONS.map((option) => (
             <SelectItem key={option.value} value={option.value}>
             <SelectItem key={option.value} value={option.value}>
-              {option.label}
+              {t(option.labelKey)}
             </SelectItem>
             </SelectItem>
           ))}
           ))}
         </SelectContent>
         </SelectContent>
@@ -462,31 +484,19 @@ type PriceFieldProps = {
   hint?: string
   hint?: string
   value: number
   value: number
   onChange: (next: number) => void
   onChange: (next: number) => void
-  showSuffix?: boolean
 }
 }
 
 
-function PriceField({
-  label,
-  hint,
-  value,
-  onChange,
-  showSuffix = true,
-}: PriceFieldProps) {
+function PriceField({ label, hint, value, onChange }: PriceFieldProps) {
   return (
   return (
-    <div className='space-y-1'>
-      <Label className='text-xs'>{label}</Label>
-      <div className='flex items-center gap-2'>
-        <DraftNumberInput
-          min={0}
-          step={0.01}
-          value={Number.isFinite(value) ? value : 0}
-          onValueChange={onChange}
-          className='w-32'
-        />
-        {showSuffix && (
-          <span className='text-muted-foreground text-xs'>{PRICE_SUFFIX}</span>
-        )}
-      </div>
+    <div className='w-36 space-y-0.5'>
+      <Label className='text-muted-foreground text-xs'>{label}</Label>
+      <DraftNumberInput
+        min={0}
+        step={0.01}
+        value={Number.isFinite(value) ? value : 0}
+        onValueChange={onChange}
+        className='h-8 w-full'
+      />
       {hint && <p className='text-muted-foreground text-xs'>{hint}</p>}
       {hint && <p className='text-muted-foreground text-xs'>{hint}</p>}
     </div>
     </div>
   )
   )
@@ -547,21 +557,49 @@ function VisualTierCard({
 
 
   const inputUnitPrice = unitCostToPrice(tier.input_unit_cost)
   const inputUnitPrice = unitCostToPrice(tier.input_unit_cost)
   const outputUnitPrice = unitCostToPrice(tier.output_unit_cost)
   const outputUnitPrice = unitCostToPrice(tier.output_unit_cost)
+  const hasMediaPricing = MEDIA_PRICE_VARS.some((variable) => {
+    const fieldKey = variable.tierField as keyof VisualTier
+    return unitCostToPrice((tier[fieldKey] as number | undefined) ?? 0) > 0
+  })
+  const [mediaOpen, setMediaOpen] = useState(hasMediaPricing)
+
+  useEffect(() => {
+    if (hasMediaPricing) setMediaOpen(true)
+  }, [hasMediaPricing])
+
+  const renderPriceVariable = (
+    variable: (typeof BILLING_EXTRA_VARS)[number]
+  ) => {
+    const fieldKey = variable.tierField as keyof VisualTier
+    const value = unitCostToPrice((tier[fieldKey] as number | undefined) ?? 0)
+
+    return (
+      <PriceField
+        key={variable.key}
+        label={t(variable.label)}
+        value={value}
+        onChange={(next) => handlePriceChange(fieldKey, priceToUnitCost(next))}
+      />
+    )
+  }
 
 
   return (
   return (
-    <div className='bg-muted/30 space-y-3 rounded-md border p-3'>
-      <div className='flex items-center justify-between gap-2'>
+    <div className='space-y-3 rounded-lg border p-3'>
+      <div className='flex flex-wrap items-center justify-between gap-2'>
         <div className='flex items-center gap-2'>
         <div className='flex items-center gap-2'>
           <Badge variant='outline'>
           <Badge variant='outline'>
             {t('Tier')} {index + 1} / {total}
             {t('Tier')} {index + 1} / {total}
           </Badge>
           </Badge>
+          {tier.conditions.length === 0 && (
+            <Badge variant='secondary'>{t('Fallback tier')}</Badge>
+          )}
           <Input
           <Input
             value={tier.label}
             value={tier.label}
             onChange={(event) =>
             onChange={(event) =>
               onChange({ ...tier, label: event.target.value })
               onChange({ ...tier, label: event.target.value })
             }
             }
             placeholder={t('Tier name')}
             placeholder={t('Tier name')}
-            className='h-8 w-40'
+            className='h-7 w-36'
           />
           />
         </div>
         </div>
         <Button
         <Button
@@ -575,9 +613,10 @@ function VisualTierCard({
         </Button>
         </Button>
       </div>
       </div>
 
 
-      <div className='space-y-2'>
-        <div className='flex items-center justify-between'>
-          <Label className='text-xs'>{t('Conditions (AND)')}</Label>
+      {/* Conditions */}
+      <div className='space-y-1.5'>
+        <div className='flex h-7 items-center justify-between'>
+          <Label className='text-xs font-medium'>{t('Tier conditions')}</Label>
           <Button
           <Button
             variant='ghost'
             variant='ghost'
             size='sm'
             size='sm'
@@ -605,68 +644,91 @@ function VisualTierCard({
         )}
         )}
       </div>
       </div>
 
 
-      <div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
-        <PriceField
-          label={t('Input price')}
-          hint={`${inputUnitPrice} × p`}
-          value={inputUnitPrice}
-          onChange={(value) =>
-            handlePriceChange('input_unit_cost', priceToUnitCost(value))
-          }
-        />
-        <PriceField
-          label={t('Output price')}
-          hint={`${outputUnitPrice} × c`}
-          value={outputUnitPrice}
-          onChange={(value) =>
-            handlePriceChange('output_unit_cost', priceToUnitCost(value))
-          }
-        />
-      </div>
-
       <div className='space-y-2'>
       <div className='space-y-2'>
-        <div className='flex items-center justify-between'>
-          <Label className='text-xs'>{t('Cache mode')}</Label>
-          <Select
-            value={cacheMode}
-            onValueChange={(value) => handleCacheModeChange(value as CacheMode)}
-          >
-            <SelectTrigger className='w-44' size='sm'>
-              <SelectValue />
-            </SelectTrigger>
-            <SelectContent>
-              <SelectItem value={CACHE_MODE_GENERIC}>
-                {t('Generic cache')}
-              </SelectItem>
-              <SelectItem value={CACHE_MODE_TIMED}>
-                {t('Timed cache (1h)')}
-              </SelectItem>
-            </SelectContent>
-          </Select>
+        <div className='flex items-center justify-between gap-3'>
+          <Label className='text-sm font-semibold'>{t('Token prices')}</Label>
+          <span className='bg-muted text-muted-foreground rounded-md px-2 py-1 text-xs'>
+            {PRICE_SUFFIX}
+          </span>
         </div>
         </div>
-        <div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
-          {BILLING_EXTRA_VARS.map((variable) => {
-            if (variable.key === 'cc1h' && cacheMode !== CACHE_MODE_TIMED) {
-              return null
-            }
-            const fieldKey = variable.tierField as keyof VisualTier
-            const value = unitCostToPrice(
-              (tier[fieldKey] as number | undefined) ?? 0
-            )
-            return (
-              <PriceField
-                key={variable.key}
-                label={variable.label}
-                hint={`${value} × ${variable.key}`}
-                value={value}
-                onChange={(next) =>
-                  handlePriceChange(fieldKey, priceToUnitCost(next))
+
+        <div className='space-y-3'>
+          <div className='flex flex-wrap gap-x-4 gap-y-2'>
+            <PriceField
+              label={t('Input price')}
+              value={inputUnitPrice}
+              onChange={(value) =>
+                handlePriceChange('input_unit_cost', priceToUnitCost(value))
+              }
+            />
+            <PriceField
+              label={t('Output price')}
+              value={outputUnitPrice}
+              onChange={(value) =>
+                handlePriceChange('output_unit_cost', priceToUnitCost(value))
+              }
+            />
+          </div>
+
+          <div className='space-y-2'>
+            <div className='flex h-7 items-center'>
+              <Tabs
+                value={cacheMode}
+                onValueChange={(value) =>
+                  value !== null && handleCacheModeChange(value as CacheMode)
                 }
                 }
-              />
-            )
-          })}
+              >
+                <TabsList className='h-8'>
+                  <TabsTrigger
+                    value={CACHE_MODE_GENERIC}
+                    className='px-2 text-xs'
+                  >
+                    {t('Generic cache')}
+                  </TabsTrigger>
+                  <TabsTrigger
+                    value={CACHE_MODE_TIMED}
+                    className='px-2 text-xs'
+                  >
+                    {t('Time-sliced cache (Claude)')}
+                  </TabsTrigger>
+                </TabsList>
+              </Tabs>
+            </div>
+            <div className='flex flex-wrap gap-x-4 gap-y-2'>
+              {CACHE_PRICE_VARS.map((variable) => {
+                if (variable.key === 'cc1h' && cacheMode !== CACHE_MODE_TIMED) {
+                  return null
+                }
+                return renderPriceVariable(variable)
+              })}
+            </div>
+          </div>
         </div>
         </div>
       </div>
       </div>
+
+      {/* Media prices */}
+      <div className='space-y-1.5'>
+        <Button
+          type='button'
+          variant='ghost'
+          size='sm'
+          className='h-7 px-2 text-xs'
+          onClick={() => setMediaOpen((prev) => !prev)}
+        >
+          <ChevronDown
+            className={cn(
+              'mr-1 h-3 w-3 transition-transform',
+              mediaOpen && 'rotate-180'
+            )}
+          />
+          {t('Media pricing')}
+        </Button>
+        {mediaOpen && (
+          <div className='flex flex-wrap gap-x-4 gap-y-2'>
+            {MEDIA_PRICE_VARS.map(renderPriceVariable)}
+          </div>
+        )}
+      </div>
     </div>
     </div>
   )
   )
 }
 }
@@ -747,14 +809,12 @@ function VisualEditor({ visualConfig, onChange }: VisualEditorProps) {
   }
   }
 
 
   return (
   return (
-    <div className='space-y-3'>
-      <Alert>
-        <AlertDescription className='text-xs'>
-          {t(
-            'Each tier supports 0~2 conditions (over len, p, c); the last tier is the catch-all without conditions. Use len (full input length, including cache hits) for tier conditions to avoid mis-routing when cache hits reduce p.'
-          )}
-        </AlertDescription>
-      </Alert>
+    <div className='space-y-2'>
+      <p className='text-muted-foreground text-xs'>
+        {t(
+          'Each tier supports up to 2 conditions. The last tier without conditions is the fallback.'
+        )}
+      </p>
       {config.tiers.map((tier, index) => (
       {config.tiers.map((tier, index) => (
         <VisualTierCard
         <VisualTierCard
           key={index}
           key={index}
@@ -766,7 +826,12 @@ function VisualEditor({ visualConfig, onChange }: VisualEditorProps) {
           onAddCondition={() => handleAddCondition(index)}
           onAddCondition={() => handleAddCondition(index)}
         />
         />
       ))}
       ))}
-      <Button variant='outline' size='sm' onClick={handleAddTier}>
+      <Button
+        variant='outline'
+        size='sm'
+        className='h-9 w-36 justify-center'
+        onClick={handleAddTier}
+      >
         <Plus className='mr-2 h-4 w-4' />
         <Plus className='mr-2 h-4 w-4' />
         {t('Add tier')}
         {t('Add tier')}
       </Button>
       </Button>
@@ -832,6 +897,50 @@ function RuleConditionRow({
 }: RuleConditionRowProps) {
 }: RuleConditionRowProps) {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const matchOptions = getRequestRuleMatchOptions(condition.source)
   const matchOptions = getRequestRuleMatchOptions(condition.source)
+  const getMatchLabel = (mode: string) => {
+    switch (mode) {
+      case MATCH_EQ:
+        return t('Equals')
+      case MATCH_CONTAINS:
+        return t('Contains')
+      case MATCH_EXISTS:
+        return t('Exists')
+      case MATCH_GT:
+        return t('Greater than')
+      case MATCH_GTE:
+        return t('Greater than or equal')
+      case MATCH_LT:
+        return t('Less than')
+      case MATCH_LTE:
+        return t('Less than or equal')
+      case MATCH_RANGE:
+        return t('Overnight range')
+      default:
+        return mode
+    }
+  }
+  const getTimeFuncLabel = (timeFunc: TimeFunc) => {
+    switch (timeFunc) {
+      case 'hour':
+        return t('Hour of day')
+      case 'minute':
+        return t('Minute')
+      case 'weekday':
+        return t('Weekday')
+      case 'month':
+        return t('Month number')
+      case 'day':
+        return t('Day of month')
+      default:
+        return timeFunc
+    }
+  }
+  const sourceLabel =
+    condition.source === SOURCE_PARAM
+      ? t('Body param')
+      : condition.source === SOURCE_HEADER
+        ? t('Header')
+        : t('Time')
 
 
   const handleSourceChange = (source: string) => {
   const handleSourceChange = (source: string) => {
     if (source === SOURCE_TIME) {
     if (source === SOURCE_TIME) {
@@ -857,12 +966,12 @@ function RuleConditionRow({
         }
         }
       >
       >
         <SelectTrigger className='w-32' size='sm'>
         <SelectTrigger className='w-32' size='sm'>
-          <SelectValue />
+          <SelectValue>{getTimeFuncLabel(timeCond.timeFunc)}</SelectValue>
         </SelectTrigger>
         </SelectTrigger>
         <SelectContent>
         <SelectContent>
           {TIME_FUNCS.map((fn) => (
           {TIME_FUNCS.map((fn) => (
             <SelectItem key={fn} value={fn}>
             <SelectItem key={fn} value={fn}>
-              {fn}
+              {getTimeFuncLabel(fn)}
             </SelectItem>
             </SelectItem>
           ))}
           ))}
         </SelectContent>
         </SelectContent>
@@ -874,7 +983,10 @@ function RuleConditionRow({
         }
         }
       >
       >
         <SelectTrigger className='w-56' size='sm'>
         <SelectTrigger className='w-56' size='sm'>
-          <SelectValue />
+          <SelectValue>
+            {COMMON_TIMEZONES.find((tz) => tz.value === timeCond.timezone)
+              ?.label ?? timeCond.timezone}
+          </SelectValue>
         </SelectTrigger>
         </SelectTrigger>
         <SelectContent>
         <SelectContent>
           {COMMON_TIMEZONES.map((tz) => (
           {COMMON_TIMEZONES.map((tz) => (
@@ -889,12 +1001,12 @@ function RuleConditionRow({
         onValueChange={(v) => v !== null && handleModeChange(v)}
         onValueChange={(v) => v !== null && handleModeChange(v)}
       >
       >
         <SelectTrigger className='w-32' size='sm'>
         <SelectTrigger className='w-32' size='sm'>
-          <SelectValue />
+          <SelectValue>{getMatchLabel(timeCond.mode)}</SelectValue>
         </SelectTrigger>
         </SelectTrigger>
         <SelectContent>
         <SelectContent>
           {matchOptions.map((option) => (
           {matchOptions.map((option) => (
             <SelectItem key={option.value} value={option.value}>
             <SelectItem key={option.value} value={option.value}>
-              {t(option.labelKey)}
+              {getMatchLabel(option.value)}
             </SelectItem>
             </SelectItem>
           ))}
           ))}
         </SelectContent>
         </SelectContent>
@@ -906,7 +1018,7 @@ function RuleConditionRow({
             onValueChange={(value) =>
             onValueChange={(value) =>
               onChange({ ...timeCond, rangeStart: String(value) })
               onChange({ ...timeCond, rangeStart: String(value) })
             }
             }
-            placeholder='start'
+            placeholder={t('Start')}
             className='w-20'
             className='w-20'
           />
           />
           <span className='text-muted-foreground text-xs'>~</span>
           <span className='text-muted-foreground text-xs'>~</span>
@@ -915,7 +1027,7 @@ function RuleConditionRow({
             onValueChange={(value) =>
             onValueChange={(value) =>
               onChange({ ...timeCond, rangeEnd: String(value) })
               onChange({ ...timeCond, rangeEnd: String(value) })
             }
             }
-            placeholder='end'
+            placeholder={t('End')}
             className='w-20'
             className='w-20'
           />
           />
         </>
         </>
@@ -925,7 +1037,7 @@ function RuleConditionRow({
           onValueChange={(value) =>
           onValueChange={(value) =>
             onChange({ ...timeCond, value: String(value) })
             onChange({ ...timeCond, value: String(value) })
           }
           }
-          placeholder='value'
+          placeholder={t('Value')}
           className='w-24'
           className='w-24'
         />
         />
       )}
       )}
@@ -947,12 +1059,12 @@ function RuleConditionRow({
         onValueChange={(v) => v !== null && handleModeChange(v)}
         onValueChange={(v) => v !== null && handleModeChange(v)}
       >
       >
         <SelectTrigger className='w-32' size='sm'>
         <SelectTrigger className='w-32' size='sm'>
-          <SelectValue />
+          <SelectValue>{getMatchLabel(phCond.mode)}</SelectValue>
         </SelectTrigger>
         </SelectTrigger>
         <SelectContent>
         <SelectContent>
           {matchOptions.map((option) => (
           {matchOptions.map((option) => (
             <SelectItem key={option.value} value={option.value}>
             <SelectItem key={option.value} value={option.value}>
-              {t(option.labelKey)}
+              {getMatchLabel(option.value)}
             </SelectItem>
             </SelectItem>
           ))}
           ))}
         </SelectContent>
         </SelectContent>
@@ -977,7 +1089,7 @@ function RuleConditionRow({
         onValueChange={(v) => v !== null && handleSourceChange(v)}
         onValueChange={(v) => v !== null && handleSourceChange(v)}
       >
       >
         <SelectTrigger className='w-28' size='sm'>
         <SelectTrigger className='w-28' size='sm'>
-          <SelectValue />
+          <SelectValue>{sourceLabel}</SelectValue>
         </SelectTrigger>
         </SelectTrigger>
         <SelectContent>
         <SelectContent>
           <SelectItem value={SOURCE_PARAM}>{t('Body param')}</SelectItem>
           <SelectItem value={SOURCE_PARAM}>{t('Body param')}</SelectItem>
@@ -1214,7 +1326,7 @@ function CostEstimator({ effectiveExpr }: EstimatorProps) {
       </div>
       </div>
       <div className='grid grid-cols-2 gap-3'>
       <div className='grid grid-cols-2 gap-3'>
         <div className='space-y-1'>
         <div className='space-y-1'>
-          <Label className='text-xs'>{t('Input tokens')} (p)</Label>
+          <Label className='text-xs'>{t('Input tokens')}</Label>
           <DraftNumberInput
           <DraftNumberInput
             min={0}
             min={0}
             value={promptTokens}
             value={promptTokens}
@@ -1222,7 +1334,7 @@ function CostEstimator({ effectiveExpr }: EstimatorProps) {
           />
           />
         </div>
         </div>
         <div className='space-y-1'>
         <div className='space-y-1'>
-          <Label className='text-xs'>{t('Output tokens')} (c)</Label>
+          <Label className='text-xs'>{t('Output tokens')}</Label>
           <DraftNumberInput
           <DraftNumberInput
             min={0}
             min={0}
             value={completionTokens}
             value={completionTokens}
@@ -1243,9 +1355,7 @@ function CostEstimator({ effectiveExpr }: EstimatorProps) {
             ) as keyof ExtraTokenValues
             ) as keyof ExtraTokenValues
             return (
             return (
               <div key={variable.key} className='space-y-1'>
               <div key={variable.key} className='space-y-1'>
-                <Label className='text-xs'>
-                  {variable.shortLabel} ({variable.key})
-                </Label>
+                <Label className='text-xs'>{t(variable.shortLabel)}</Label>
                 <DraftNumberInput
                 <DraftNumberInput
                   min={0}
                   min={0}
                   value={extras[stateKey]}
                   value={extras[stateKey]}
@@ -1612,7 +1722,7 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
         <div className='flex-1'>
         <div className='flex-1'>
           <PresetSection applyPreset={applyPreset} />
           <PresetSection applyPreset={applyPreset} />
         </div>
         </div>
-        <LlmPromptHelper modelName={modelName} />
+        {editorMode === 'raw' && <LlmPromptHelper modelName={modelName} />}
       </div>
       </div>
 
 
       <div className='bg-muted/30 space-y-3 rounded-md border p-3'>
       <div className='bg-muted/30 space-y-3 rounded-md border p-3'>
@@ -1665,34 +1775,20 @@ export const TieredPricingEditor = memo(function TieredPricingEditor({
                     }
                     }
                   />
                   />
                 ))}
                 ))}
-                <div className='flex flex-wrap gap-2'>
-                  <Button
-                    variant='outline'
-                    size='sm'
-                    onClick={() =>
-                      handleRuleGroupsChange([
-                        ...requestRuleGroups,
-                        createEmptyRuleGroup(),
-                      ])
-                    }
-                  >
-                    <Plus className='mr-2 h-4 w-4' />
-                    {t('Add rule group')}
-                  </Button>
-                  <Button
-                    variant='ghost'
-                    size='sm'
-                    onClick={() =>
-                      handleRuleGroupsChange([
-                        ...requestRuleGroups,
-                        createEmptyTimeRuleGroup(),
-                      ])
-                    }
-                  >
-                    <Plus className='mr-2 h-4 w-4' />
-                    {t('Add time rule group')}
-                  </Button>
-                </div>
+                <Button
+                  variant='outline'
+                  size='sm'
+                  className='h-9 w-36 justify-center'
+                  onClick={() =>
+                    handleRuleGroupsChange([
+                      ...requestRuleGroups,
+                      createEmptyRuleGroup(),
+                    ])
+                  }
+                >
+                  <Plus className='mr-2 h-4 w-4' />
+                  {t('Add rule group')}
+                </Button>
               </>
               </>
             )}
             )}
           </div>
           </div>

+ 95 - 0
web/default/src/features/system-settings/operations/index.tsx

@@ -0,0 +1,95 @@
+import { useMemo } from 'react'
+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 type { OperationsSettings } from '../types'
+import {
+  OPERATIONS_DEFAULT_SECTION,
+  getOperationsSectionContent,
+} from './section-registry.tsx'
+
+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/operations/$section',
+  })
+
+  const settings = useMemo(
+    () => getOptionValue(data?.data, defaultOperationsSettings),
+    [data?.data]
+  )
+
+  if (isLoading) {
+    return (
+      <div className='text-muted-foreground flex h-full w-full flex-1 items-center justify-center'>
+        {t('Loading maintenance settings...')}
+      </div>
+    )
+  }
+
+  const activeSection = (params?.section ?? OPERATIONS_DEFAULT_SECTION) as
+    | 'behavior'
+    | 'monitoring'
+    | 'email'
+    | 'worker'
+    | 'logs'
+    | 'performance'
+    | 'update-checker'
+  const sectionContent = getOperationsSectionContent(
+    activeSection,
+    settings,
+    status?.version as string | undefined,
+    status?.start_time as number | null | undefined
+  )
+
+  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>
+  )
+}

+ 163 - 0
web/default/src/features/system-settings/operations/section-registry.tsx

@@ -0,0 +1,163 @@
+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

+ 9 - 9
web/default/src/features/system-settings/request-limits/index.tsx → web/default/src/features/system-settings/security/index.tsx

@@ -1,11 +1,11 @@
 import { SettingsPage } from '../components/settings-page'
 import { SettingsPage } from '../components/settings-page'
-import type { RequestLimitsSettings } from '../types'
+import type { SecuritySettings } from '../types'
 import {
 import {
-  REQUEST_LIMITS_DEFAULT_SECTION,
-  getRequestLimitsSectionContent,
+  SECURITY_DEFAULT_SECTION,
+  getSecuritySectionContent,
 } from './section-registry.tsx'
 } from './section-registry.tsx'
 
 
-const defaultRequestLimitsSettings: RequestLimitsSettings = {
+const defaultSecuritySettings: SecuritySettings = {
   ModelRequestRateLimitEnabled: false,
   ModelRequestRateLimitEnabled: false,
   ModelRequestRateLimitCount: 0,
   ModelRequestRateLimitCount: 0,
   ModelRequestRateLimitSuccessCount: 1000,
   ModelRequestRateLimitSuccessCount: 1000,
@@ -24,13 +24,13 @@ const defaultRequestLimitsSettings: RequestLimitsSettings = {
   'fetch_setting.apply_ip_filter_for_domain': false,
   'fetch_setting.apply_ip_filter_for_domain': false,
 }
 }
 
 
-export function RequestLimitsSettings() {
+export function SecuritySettings() {
   return (
   return (
     <SettingsPage
     <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}
     />
     />
   )
   )
 }
 }

+ 18 - 22
web/default/src/features/system-settings/request-limits/section-registry.tsx → web/default/src/features/system-settings/security/section-registry.tsx

@@ -1,15 +1,15 @@
-import type { RequestLimitsSettings } from '../types'
+import type { SecuritySettings } from '../types'
 import { createSectionRegistry } from '../utils/section-registry'
 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',
     id: 'rate-limit',
     titleKey: 'Rate Limiting',
     titleKey: 'Rate Limiting',
     descriptionKey: 'Configure model request rate limiting',
     descriptionKey: 'Configure model request rate limiting',
-    build: (settings: RequestLimitsSettings) => (
+    build: (settings: SecuritySettings) => (
       <RateLimitSection
       <RateLimitSection
         defaultValues={{
         defaultValues={{
           ModelRequestRateLimitEnabled: settings.ModelRequestRateLimitEnabled,
           ModelRequestRateLimitEnabled: settings.ModelRequestRateLimitEnabled,
@@ -27,7 +27,7 @@ const REQUEST_LIMITS_SECTIONS = [
     id: 'sensitive-words',
     id: 'sensitive-words',
     titleKey: 'Sensitive Words',
     titleKey: 'Sensitive Words',
     descriptionKey: 'Configure sensitive word filtering',
     descriptionKey: 'Configure sensitive word filtering',
-    build: (settings: RequestLimitsSettings) => (
+    build: (settings: SecuritySettings) => (
       <SensitiveWordsSection
       <SensitiveWordsSection
         defaultValues={{
         defaultValues={{
           CheckSensitiveEnabled: settings.CheckSensitiveEnabled,
           CheckSensitiveEnabled: settings.CheckSensitiveEnabled,
@@ -41,7 +41,7 @@ const REQUEST_LIMITS_SECTIONS = [
     id: 'ssrf',
     id: 'ssrf',
     titleKey: 'SSRF Protection',
     titleKey: 'SSRF Protection',
     descriptionKey: 'Configure SSRF (Server-Side Request Forgery) protection',
     descriptionKey: 'Configure SSRF (Server-Side Request Forgery) protection',
-    build: (settings: RequestLimitsSettings) => (
+    build: (settings: SecuritySettings) => (
       <SSRFSection
       <SSRFSection
         defaultValues={{
         defaultValues={{
           'fetch_setting.enable_ssrf_protection':
           'fetch_setting.enable_ssrf_protection':
@@ -64,23 +64,19 @@ const REQUEST_LIMITS_SECTIONS = [
   },
   },
 ] as const
 ] 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',
   defaultSection: 'rate-limit',
-  basePath: '/system-settings/request-limits',
+  basePath: '/system-settings/security',
   urlStyle: 'path',
   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

+ 32 - 0
web/default/src/features/system-settings/site/index.tsx

@@ -0,0 +1,32 @@
+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}
+    />
+  )
+}

+ 93 - 0
web/default/src/features/system-settings/site/section-registry.tsx

@@ -0,0 +1,93 @@
+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

+ 110 - 92
web/default/src/features/system-settings/types.ts

@@ -27,7 +27,7 @@ export type DeleteLogsResponse = {
   data?: number
   data?: number
 }
 }
 
 
-export type GeneralSettings = {
+export type SiteSettings = {
   'theme.frontend': string
   'theme.frontend': string
   Notice: string
   Notice: string
   SystemName: string
   SystemName: string
@@ -38,32 +38,8 @@ export type GeneralSettings = {
   ServerAddress: string
   ServerAddress: string
   'legal.user_agreement': string
   'legal.user_agreement': string
   'legal.privacy_policy': 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 = {
 export type AuthSettings = {
@@ -131,28 +107,87 @@ export type ContentSettings = {
   MjActionCheckSuccessEnabled: boolean
   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.api_key': string
   'model_deployment.ionet.enabled': boolean
   '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
   PayAddress: string
   EpayId: string
   EpayId: string
   EpayKey: string
   EpayKey: string
@@ -199,53 +234,36 @@ export type IntegrationSettings = {
   WaffoPancakeCurrency: string
   WaffoPancakeCurrency: string
   WaffoPancakeUnitPrice: number
   WaffoPancakeUnitPrice: number
   WaffoPancakeMinTopUp: 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
   LogConsumeEnabled: boolean
-  HeaderNavModules: string
-  SidebarModulesAdmin: string
   'performance_setting.disk_cache_enabled': boolean
   'performance_setting.disk_cache_enabled': boolean
   'performance_setting.disk_cache_threshold_mb': number
   'performance_setting.disk_cache_threshold_mb': number
   'performance_setting.disk_cache_max_size_mb': number
   'performance_setting.disk_cache_max_size_mb': number
@@ -260,7 +278,7 @@ export type MaintenanceSettings = {
   'perf_metrics_setting.retention_days': number
   'perf_metrics_setting.retention_days': number
 }
 }
 
 
-export type RequestLimitsSettings = {
+export type SecuritySettings = {
   ModelRequestRateLimitEnabled: boolean
   ModelRequestRateLimitEnabled: boolean
   ModelRequestRateLimitCount: number
   ModelRequestRateLimitCount: number
   ModelRequestRateLimitSuccessCount: number
   ModelRequestRateLimitSuccessCount: number

+ 6 - 6
web/default/src/features/system-settings/utils/route-config.ts

@@ -29,7 +29,7 @@ export type SettingsRouteConfigOptions<
   defaultSection: TSectionId
   defaultSection: TSectionId
   /** Settings component to render */
   /** Settings component to render */
   component: TComponent
   component: TComponent
-  /** Route path for redirect (e.g., '/system-settings/general') */
+  /** Route path for redirect (e.g., '/system-settings/site') */
   routePath: string
   routePath: string
   /** Whether to redirect to default section if no section is provided (default: false) */
   /** Whether to redirect to default section if no section is provided (default: false) */
   redirectToDefault?: boolean
   redirectToDefault?: boolean
@@ -44,12 +44,12 @@ export type SettingsRouteConfigOptions<
  *
  *
  * @example
  * @example
  * ```tsx
  * ```tsx
- * export const Route = createFileRoute('/_authenticated/system-settings/general')(
+   * export const Route = createFileRoute('/_authenticated/system-settings/site')(
  *   createSettingsRouteConfig({
  *   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,
  *     redirectToDefault: true,
  *   })
  *   })
  * )
  * )

+ 1 - 1
web/default/src/hooks/use-sidebar-config.ts

@@ -96,7 +96,7 @@ const URL_TO_CONFIG_MAP: Record<string, { section: string; module: string }> = {
   '/redemption-codes': { section: 'admin', module: 'redemption' },
   '/redemption-codes': { section: 'admin', module: 'redemption' },
   '/subscriptions': { section: 'admin', module: 'subscription' },
   '/subscriptions': { section: 'admin', module: 'subscription' },
   '/system-settings': { section: 'admin', module: 'setting' },
   '/system-settings': { section: 'admin', module: 'setting' },
-  '/system-settings/general': { section: 'admin', module: 'setting' },
+  '/system-settings/site': { section: 'admin', module: 'setting' },
 }
 }
 
 
 /**
 /**

+ 1 - 1
web/default/src/hooks/use-sidebar-data.ts

@@ -129,7 +129,7 @@ export function useSidebarData(): SidebarData {
           },
           },
           {
           {
             title: t('System Settings'),
             title: t('System Settings'),
-            url: '/system-settings/general',
+            url: '/system-settings/site',
             activeUrls: ['/system-settings'],
             activeUrls: ['/system-settings'],
             icon: Settings,
             icon: Settings,
           },
           },

+ 4 - 4
web/default/src/i18n/locales/_reports/_sync-report.json

@@ -11,25 +11,25 @@
       "file": "fr.json",
       "file": "fr.json",
       "missingCount": 0,
       "missingCount": 0,
       "extrasCount": 0,
       "extrasCount": 0,
-      "untranslatedCount": 0
+      "untranslatedCount": 1
     },
     },
     "ja": {
     "ja": {
       "file": "ja.json",
       "file": "ja.json",
       "missingCount": 0,
       "missingCount": 0,
       "extrasCount": 0,
       "extrasCount": 0,
-      "untranslatedCount": 88
+      "untranslatedCount": 90
     },
     },
     "ru": {
     "ru": {
       "file": "ru.json",
       "file": "ru.json",
       "missingCount": 0,
       "missingCount": 0,
       "extrasCount": 0,
       "extrasCount": 0,
-      "untranslatedCount": 92
+      "untranslatedCount": 105
     },
     },
     "vi": {
     "vi": {
       "file": "vi.json",
       "file": "vi.json",
       "missingCount": 0,
       "missingCount": 0,
       "extrasCount": 0,
       "extrasCount": 0,
-      "untranslatedCount": 0
+      "untranslatedCount": 3
     },
     },
     "zh": {
     "zh": {
       "file": "zh.json",
       "file": "zh.json",

+ 1 - 2
web/default/src/i18n/locales/_reports/fr.untranslated.json

@@ -1,4 +1,3 @@
 {
 {
-  "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."
 }
 }

+ 2 - 0
web/default/src/i18n/locales/_reports/ja.untranslated.json

@@ -77,6 +77,8 @@
   "Submodel": "Submodel",
   "Submodel": "Submodel",
   "SunoAPI": "SunoAPI",
   "SunoAPI": "SunoAPI",
   "Telegram": "Telegram",
   "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 P50": "TTFT P50",
   "TTFT P95": "TTFT P95",
   "TTFT P95": "TTFT P95",
   "TTFT P99": "TTFT P99",
   "TTFT P99": "TTFT P99",

+ 14 - 1
web/default/src/i18n/locales/_reports/ru.untranslated.json

@@ -81,6 +81,8 @@
   "SunoAPI": "SunoAPI",
   "SunoAPI": "SunoAPI",
   "Telegram": "Telegram",
   "Telegram": "Telegram",
   "Tencent": "Tencent",
   "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 P50": "TTFT P50",
   "TTFT P95": "TTFT P95",
   "TTFT P95": "TTFT P95",
   "TTFT P99": "TTFT P99",
   "TTFT P99": "TTFT P99",
@@ -90,5 +92,16 @@
   "whsec_xxx": "whsec_xxx",
   "whsec_xxx": "whsec_xxx",
   "Xinference": "Xinference",
   "Xinference": "Xinference",
   "Xunfei": "Xunfei",
   "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"
 }
 }

+ 3 - 2
web/default/src/i18n/locales/_reports/vi.untranslated.json

@@ -1,4 +1,5 @@
 {
 {
-  "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."
 }
 }

+ 93 - 2
web/default/src/i18n/locales/en.json

@@ -494,6 +494,7 @@
     "Best for single-tenant deployments. Pricing and billing options stay hidden.": "Best for single-tenant deployments. Pricing and billing options stay hidden.",
     "Best for single-tenant deployments. Pricing and billing options stay hidden.": "Best for single-tenant deployments. Pricing and billing options stay hidden.",
     "Best TTFT": "Best TTFT",
     "Best TTFT": "Best TTFT",
     "Billing": "Billing",
     "Billing": "Billing",
+    "Billing & Payment": "Billing & Payment",
     "Billing currency": "Billing currency",
     "Billing currency": "Billing currency",
     "Billing Details": "Billing Details",
     "Billing Details": "Billing Details",
     "Billing History": "Billing History",
     "Billing History": "Billing History",
@@ -633,6 +634,7 @@
     "Check in now": "Check in now",
     "Check in now": "Check in now",
     "Check resolved IPs against IP filters even when accessing by domain": "Check resolved IPs against IP filters even when accessing by domain",
     "Check resolved IPs against IP filters even when accessing by domain": "Check resolved IPs against IP filters even when accessing by domain",
     "Check-in failed": "Check-in failed",
     "Check-in failed": "Check-in failed",
+    "Check-in Rewards": "Check-in Rewards",
     "Check-in Settings": "Check-in Settings",
     "Check-in Settings": "Check-in Settings",
     "Check-in successful! Received": "Check-in successful! Received",
     "Check-in successful! Received": "Check-in successful! Received",
     "Checked in": "Checked in",
     "Checked in": "Checked in",
@@ -783,14 +785,18 @@
     "Configure basic system information and branding": "Configure basic system information and branding",
     "Configure basic system information and branding": "Configure basic system information and branding",
     "Configure channel affinity (sticky routing) rules": "Configure channel affinity (sticky routing) rules",
     "Configure channel affinity (sticky routing) rules": "Configure channel affinity (sticky routing) rules",
     "Configure Creem products. Provide a JSON array.": "Configure Creem products. Provide a JSON array.",
     "Configure Creem products. Provide a JSON array.": "Configure Creem products. Provide a JSON array.",
+    "Configure currency conversion and quota display options": "Configure currency conversion and quota display options",
     "Configure custom OAuth providers for user authentication": "Configure custom OAuth providers for user authentication",
     "Configure custom OAuth providers for user authentication": "Configure custom OAuth providers for user authentication",
     "Configure daily check-in rewards for users": "Configure daily check-in rewards for users",
     "Configure daily check-in rewards for users": "Configure daily check-in rewards for users",
     "Configure discount rates based on recharge amounts": "Configure discount rates based on recharge amounts",
     "Configure discount rates based on recharge amounts": "Configure discount rates based on recharge amounts",
     "Configure experimental data export for the dashboard": "Configure experimental data export for the dashboard",
     "Configure experimental data export for the dashboard": "Configure experimental data export for the dashboard",
     "Configure Gemini safety behavior, version overrides, and thinking adapter": "Configure Gemini safety behavior, version overrides, and thinking adapter",
     "Configure Gemini safety behavior, version overrides, and thinking adapter": "Configure Gemini safety behavior, version overrides, and thinking adapter",
+    "Configure group ratios and group-specific pricing rules": "Configure group ratios and group-specific pricing rules",
     "Configure in your Creem dashboard": "Configure in your Creem dashboard",
     "Configure in your Creem dashboard": "Configure in your Creem dashboard",
     "Configure io.net API key for model deployments": "Configure io.net API key for model deployments",
     "Configure io.net API key for model deployments": "Configure io.net API key for model deployments",
     "Configure keyword filtering for prompts and responses.": "Configure keyword filtering for prompts and responses.",
     "Configure keyword filtering for prompts and responses.": "Configure keyword filtering for prompts and responses.",
+    "Configure model deployment provider settings": "Configure model deployment provider settings",
+    "Configure model pricing ratios and tool prices": "Configure model pricing ratios and tool prices",
     "Configure model, caching, and group ratios used for billing": "Configure model, caching, and group ratios used for billing",
     "Configure model, caching, and group ratios used for billing": "Configure model, caching, and group ratios used for billing",
     "Configure monitoring status page groups for the dashboard": "Configure monitoring status page groups for the dashboard",
     "Configure monitoring status page groups for the dashboard": "Configure monitoring status page groups for the dashboard",
     "Configure outgoing email server for notifications": "Configure outgoing email server for notifications",
     "Configure outgoing email server for notifications": "Configure outgoing email server for notifications",
@@ -849,6 +855,7 @@
     "Console": "Console",
     "Console": "Console",
     "Console area": "Console area",
     "Console area": "Console area",
     "Console Area": "Console Area",
     "Console Area": "Console Area",
+    "Console Content": "Console Content",
     "Consume": "Consume",
     "Consume": "Consume",
     "Container": "Container",
     "Container": "Container",
     "Container name": "Container name",
     "Container name": "Container name",
@@ -970,6 +977,7 @@
     "Cross-group retry": "Cross-group retry",
     "Cross-group retry": "Cross-group retry",
     "Curate quick links to your different Domains": "Curate quick links to your different Domains",
     "Curate quick links to your different Domains": "Curate quick links to your different Domains",
     "Currency": "Currency",
     "Currency": "Currency",
+    "Currency & Display": "Currency & Display",
     "Current Balance": "Current Balance",
     "Current Balance": "Current Balance",
     "Current Billing": "Current Billing",
     "Current Billing": "Current Billing",
     "Current Cache Size": "Current Cache Size",
     "Current Cache Size": "Current Cache Size",
@@ -1027,6 +1035,7 @@
     "Date and time when this announcement should be displayed": "Date and time when this announcement should be displayed",
     "Date and time when this announcement should be displayed": "Date and time when this announcement should be displayed",
     "Date Range": "Date Range",
     "Date Range": "Date Range",
     "Day": "Day",
     "Day": "Day",
+    "Day of month": "Day of month",
     "days": "days",
     "days": "days",
     "Days to Retain": "Days to Retain",
     "Days to Retain": "Days to Retain",
     "Deducted by subscription": "Deducted by subscription",
     "Deducted by subscription": "Deducted by subscription",
@@ -1778,6 +1787,8 @@
     "GPU count": "GPU count",
     "GPU count": "GPU count",
     "Greater Than": "Greater Than",
     "Greater Than": "Greater Than",
     "Greater Than or Equal": "Greater Than or Equal",
     "Greater Than or Equal": "Greater Than or Equal",
+    "Greater than": "Greater than",
+    "Greater than or equal": "Greater than or equal",
     "Grok": "Grok",
     "Grok": "Grok",
     "Grok Settings": "Grok Settings",
     "Grok Settings": "Grok Settings",
     "Group": "Group",
     "Group": "Group",
@@ -1793,6 +1804,7 @@
     "Group Name": "Group Name",
     "Group Name": "Group Name",
     "Group name cannot be changed when editing.": "Group name cannot be changed when editing.",
     "Group name cannot be changed when editing.": "Group name cannot be changed when editing.",
     "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.": "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.",
     "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.": "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.",
+    "Group Pricing": "Group Pricing",
     "group ratio": "group ratio",
     "group ratio": "group ratio",
     "Group Ratio": "Group Ratio",
     "Group Ratio": "Group Ratio",
     "Group ratios": "Group ratios",
     "Group ratios": "Group ratios",
@@ -1848,6 +1860,7 @@
     "Home Page Content": "Home Page Content",
     "Home Page Content": "Home Page Content",
     "Hostname or IP of your SMTP provider": "Hostname or IP of your SMTP provider",
     "Hostname or IP of your SMTP provider": "Hostname or IP of your SMTP provider",
     "Hour": "Hour",
     "Hour": "Hour",
+    "Hour of day": "Hour of day",
     "Hourly token usage by model across the last 24 hours": "Hourly token usage by model across the last 24 hours",
     "Hourly token usage by model across the last 24 hours": "Hourly token usage by model across the last 24 hours",
     "Hourly token usage by model over the past 24 hours": "Hourly token usage by model over the past 24 hours",
     "Hourly token usage by model over the past 24 hours": "Hourly token usage by model over the past 24 hours",
     "hours": "hours",
     "hours": "hours",
@@ -1893,6 +1906,7 @@
     "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.",
     "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.",
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.",
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing",
+    "If this keeps happening, please report it on GitHub Issues.": "If this keeps happening, please report it on GitHub Issues.",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.",
     "Ignored upstream models": "Ignored upstream models",
     "Ignored upstream models": "Ignored upstream models",
     "Image": "Image",
     "Image": "Image",
@@ -2056,6 +2070,8 @@
     "Less": "Less",
     "Less": "Less",
     "Less Than": "Less Than",
     "Less Than": "Less Than",
     "Less Than or Equal": "Less Than or Equal",
     "Less Than or Equal": "Less Than or Equal",
+    "Less than": "Less than",
+    "Less than or equal": "Less than or equal",
     "License": "License",
     "License": "License",
     "Light": "Light",
     "Light": "Light",
     "Lightning Fast": "Lightning Fast",
     "Lightning Fast": "Lightning Fast",
@@ -2217,6 +2233,7 @@
     "Model context usage": "Model context usage",
     "Model context usage": "Model context usage",
     "Model deleted": "Model deleted",
     "Model deleted": "Model deleted",
     "Model deleted successfully": "Model deleted successfully",
     "Model deleted successfully": "Model deleted successfully",
+    "Model Deployment": "Model Deployment",
     "Model deployment service is disabled": "Model deployment service is disabled",
     "Model deployment service is disabled": "Model deployment service is disabled",
     "Model Description": "Model Description",
     "Model Description": "Model Description",
     "Model details": "Model details",
     "Model details": "Model details",
@@ -2256,6 +2273,7 @@
     "Models": "Models",
     "Models": "Models",
     "Models *": "Models *",
     "Models *": "Models *",
     "Models & Groups": "Models & Groups",
     "Models & Groups": "Models & Groups",
+    "Models & Routing": "Models & Routing",
     "Models appended successfully": "Models appended successfully",
     "Models appended successfully": "Models appended successfully",
     "Models are required": "Models are required",
     "Models are required": "Models are required",
     "Models climbing the leaderboard": "Models climbing the leaderboard",
     "Models climbing the leaderboard": "Models climbing the leaderboard",
@@ -2276,6 +2294,7 @@
     "Monitor": "Monitor",
     "Monitor": "Monitor",
     "Monitoring & Alerts": "Monitoring & Alerts",
     "Monitoring & Alerts": "Monitoring & Alerts",
     "Month": "Month",
     "Month": "Month",
+    "Month number": "Month number",
     "Monthly": "Monthly",
     "Monthly": "Monthly",
     "Monthly tokens": "Monthly tokens",
     "Monthly tokens": "Monthly tokens",
     "months": "months",
     "months": "months",
@@ -2592,6 +2611,7 @@
     "Operation": "Operation",
     "Operation": "Operation",
     "Operation failed": "Operation failed",
     "Operation failed": "Operation failed",
     "Operation Type": "Operation Type",
     "Operation Type": "Operation Type",
+    "Operations": "Operations",
     "Operator Admin": "Operator Admin",
     "Operator Admin": "Operator Admin",
     "Optimize system for self-hosted single-user usage": "Optimize system for self-hosted single-user usage",
     "Optimize system for self-hosted single-user usage": "Optimize system for self-hosted single-user usage",
     "Optimized network architecture ensures millisecond response times": "Optimized network architecture ensures millisecond response times",
     "Optimized network architecture ensures millisecond response times": "Optimized network architecture ensures millisecond response times",
@@ -2636,6 +2656,7 @@
     "Override Rules": "Override Rules",
     "Override Rules": "Override Rules",
     "Override the endpoint used for testing. Leave empty to auto detect.": "Override the endpoint used for testing. Leave empty to auto detect.",
     "Override the endpoint used for testing. Leave empty to auto detect.": "Override the endpoint used for testing. Leave empty to auto detect.",
     "overrides for matching model prefix.": "overrides for matching model prefix.",
     "overrides for matching model prefix.": "overrides for matching model prefix.",
+    "Overnight range": "Overnight range",
     "Overview": "Overview",
     "Overview": "Overview",
     "Overwritten": "Overwritten",
     "Overwritten": "Overwritten",
     "Page": "Page",
     "Page": "Page",
@@ -3102,6 +3123,7 @@
     "Replacement Model": "Replacement Model",
     "Replacement Model": "Replacement Model",
     "Replica count": "Replica count",
     "Replica count": "Replica count",
     "Replicate": "Replicate",
     "Replicate": "Replicate",
+    "Report an issue": "Report an issue",
     "request": "request",
     "request": "request",
     "Request": "Request",
     "Request": "Request",
     "Request Body Disk Cache": "Request Body Disk Cache",
     "Request Body Disk Cache": "Request Body Disk Cache",
@@ -3308,6 +3330,7 @@
     "Secret Key": "Secret Key",
     "Secret Key": "Secret Key",
     "Secure & Reliable": "Secure & Reliable",
     "Secure & Reliable": "Secure & Reliable",
     "Security": "Security",
     "Security": "Security",
+    "Security & Limits": "Security & Limits",
     "Security Check": "Security Check",
     "Security Check": "Security Check",
     "Security verification": "Security verification",
     "Security verification": "Security verification",
     "Select": "Select",
     "Select": "Select",
@@ -3456,6 +3479,7 @@
     "Simple mode only returns message; status code and error type use system defaults.": "Simple mode only returns message; status code and error type use system defaults.",
     "Simple mode only returns message; status code and error type use system defaults.": "Simple mode only returns message; status code and error type use system defaults.",
     "Simple mode: prune objects by type, e.g. redacted_thinking.": "Simple mode: prune objects by type, e.g. redacted_thinking.",
     "Simple mode: prune objects by type, e.g. redacted_thinking.": "Simple mode: prune objects by type, e.g. redacted_thinking.",
     "Single Key": "Single Key",
     "Single Key": "Single Key",
+    "Site & Branding": "Site & Branding",
     "Site Key": "Site Key",
     "Site Key": "Site Key",
     "Size:": "Size:",
     "Size:": "Size:",
     "sk_xxx or rk_xxx": "sk_xxx or rk_xxx",
     "sk_xxx or rk_xxx": "sk_xxx or rk_xxx",
@@ -3733,6 +3757,8 @@
     "Tier": "Tier",
     "Tier": "Tier",
     "Tier name": "Tier name",
     "Tier name": "Tier name",
     "Tiered": "Tiered",
     "Tiered": "Tiered",
+    "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.",
     "Tiered (billing expression)": "Tiered (billing expression)",
     "Tiered (billing expression)": "Tiered (billing expression)",
     "Tiered price table": "Tiered price table",
     "Tiered price table": "Tiered price table",
     "Time": "Time",
     "Time": "Time",
@@ -3741,7 +3767,7 @@
     "Time window for rate limiting": "Time window for rate limiting",
     "Time window for rate limiting": "Time window for rate limiting",
     "Time-based": "Time-based",
     "Time-based": "Time-based",
     "Time:": "Time:",
     "Time:": "Time:",
-    "Timed cache (1h)": "Timed cache (1h)",
+    "Time-sliced cache (Claude)": "Time-sliced cache (Claude)",
     "Timeline": "Timeline",
     "Timeline": "Timeline",
     "times": "times",
     "times": "times",
     "Timing": "Timing",
     "Timing": "Timing",
@@ -4219,6 +4245,71 @@
     "Zero retention": "Zero retention",
     "Zero retention": "Zero retention",
     "Zhipu": "Zhipu",
     "Zhipu": "Zhipu",
     "Zhipu V4": "Zhipu V4",
     "Zhipu V4": "Zhipu V4",
-    "Zoom": "Zoom"
+    "Zoom": "Zoom",
+    "Add model pricing": "Add model pricing",
+    "Applied {{name}} pricing to {{count}} models": "Applied {{name}} pricing to {{count}} models",
+    "Audio input price": "Audio input price",
+    "Audio output price": "Audio output price",
+    "Audio output price requires an audio input price.": "Audio output price requires an audio input price.",
+    "Billable input tokens": "Billable input tokens",
+    "Billable output tokens": "Billable output tokens",
+    "Cache create (1h) price": "Cache create (1h) price",
+    "Cache create price": "Cache create price",
+    "Cache read price": "Cache read price",
+    "Cache pricing": "Cache pricing",
+    "Cache write price": "Cache write price",
+    "Changes are written to the settings draft on save.": "Changes are written to the settings draft on save.",
+    "Clean": "Clean",
+    "Completion price": "Completion price",
+    "Core pricing": "Core pricing",
+    "Copy {{name}} pricing": "Copy {{name}} pricing",
+    "Disabled lanes are omitted on save.": "Disabled lanes are omitted on save.",
+    "Edit model pricing": "Edit model pricing",
+    "Empty": "Empty",
+    "Each tier supports up to 2 conditions; the last tier is the catch-all without conditions. Use full input length for tier conditions to avoid mis-routing when cache hits reduce billable input tokens.": "Each tier supports up to 2 conditions; the last tier is the catch-all without conditions. Use full input length for tier conditions to avoid mis-routing when cache hits reduce billable input tokens.",
+    "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.",
+    "Expression": "Expression",
+    "Fallback tier": "Fallback tier",
+    "Full input length": "Full input length",
+    "Input price is required before saving dependent prices.": "Input price is required before saving dependent prices.",
+    "Image input price": "Image input price",
+    "Image output price": "Image output price",
+    "Media pricing": "Media pricing",
+    "New model": "New model",
+    "No separate media pricing configured.": "No separate media pricing configured.",
+    "Open a source model first": "Open a source model first",
+    "Output token price for generated tokens.": "Output token price for generated tokens.",
+    "Per-request": "Per-request",
+    "Save preview": "Save preview",
+    "Select at least one target model": "Select at least one target model",
+    "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 model has both fixed-price and ratio settings. Saving the current mode will rewrite the conflicting fields.": "This model has both fixed-price and ratio settings. Saving the current mode will rewrite the conflicting fields.",
+    "This model has both fixed-price and token-price settings. Saving the current mode will rewrite the conflicting fields.": "This model has both fixed-price and token-price settings. Saving the current mode will rewrite the conflicting fields.",
+    "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",
+    "Token price for audio input.": "Token price for audio input.",
+    "Token price for audio output.": "Token price for audio output.",
+    "Token price for cache reads.": "Token price for cache reads.",
+    "Token price for creating cache entries.": "Token price for creating cache entries.",
+    "Token price for image input.": "Token price for image input.",
+    "USD price per 1M input tokens.": "USD price per 1M input tokens.",
+    "USD price per 1M tokens.": "USD price per 1M tokens.",
+    "{{count}} selected targets available for bulk copy.": "{{count}} selected targets available for bulk copy.",
+    "Base input price only": "Base input price only",
+    "Expression based": "Expression based",
+    "Expression pricing": "Expression pricing",
+    "Fixed request price": "Fixed request price",
+    "Includes request rules": "Includes request rules",
+    "No base input price": "No base input price",
+    "No models configured. Use Add model to get started.": "No models configured. Use Add model to get started.",
+    "Price summary": "Price summary",
+    "Select a model to edit pricing": "Select a model to edit pricing",
+    "Tiered pricing": "Tiered pricing",
+    "Unset price": "Unset price",
+    "Use the full-width table to scan prices, then select a row to edit it here.": "Use the full-width table to scan prices, then select a row to edit it here.",
+    "extras": "extras",
+    "tiers": "tiers"
   }
   }
 }
 }

+ 93 - 2
web/default/src/i18n/locales/fr.json

@@ -494,6 +494,7 @@
     "Best for single-tenant deployments. Pricing and billing options stay hidden.": "Idéal pour les déploiements mono-utilisateur. Les options de tarification et de facturation restent masquées.",
     "Best for single-tenant deployments. Pricing and billing options stay hidden.": "Idéal pour les déploiements mono-utilisateur. Les options de tarification et de facturation restent masquées.",
     "Best TTFT": "Meilleur TTFT",
     "Best TTFT": "Meilleur TTFT",
     "Billing": "Facturation",
     "Billing": "Facturation",
+    "Billing & Payment": "Facturation et paiement",
     "Billing currency": "Devise de facturation",
     "Billing currency": "Devise de facturation",
     "Billing Details": "Détails de facturation",
     "Billing Details": "Détails de facturation",
     "Billing History": "Historique de facturation",
     "Billing History": "Historique de facturation",
@@ -633,6 +634,7 @@
     "Check in now": "Se connecter maintenant",
     "Check in now": "Se connecter maintenant",
     "Check resolved IPs against IP filters even when accessing by domain": "Vérifier les adresses IP résolues par rapport aux filtres IP même lors de l'accès par domaine",
     "Check resolved IPs against IP filters even when accessing by domain": "Vérifier les adresses IP résolues par rapport aux filtres IP même lors de l'accès par domaine",
     "Check-in failed": "Échec de la connexion",
     "Check-in failed": "Échec de la connexion",
+    "Check-in Rewards": "Récompenses de connexion quotidienne",
     "Check-in Settings": "Paramètres de connexion",
     "Check-in Settings": "Paramètres de connexion",
     "Check-in successful! Received": "Connexion réussie ! Reçu",
     "Check-in successful! Received": "Connexion réussie ! Reçu",
     "Checked in": "Vérifié",
     "Checked in": "Vérifié",
@@ -783,14 +785,18 @@
     "Configure basic system information and branding": "Configurer les informations système de base et l'image de marque",
     "Configure basic system information and branding": "Configurer les informations système de base et l'image de marque",
     "Configure channel affinity (sticky routing) rules": "Configurer les règles d'affinité de canal (routage persistant)",
     "Configure channel affinity (sticky routing) rules": "Configurer les règles d'affinité de canal (routage persistant)",
     "Configure Creem products. Provide a JSON array.": "Configurez les produits Creem. Fournissez un tableau JSON.",
     "Configure Creem products. Provide a JSON array.": "Configurez les produits Creem. Fournissez un tableau JSON.",
+    "Configure currency conversion and quota display options": "Configurer la conversion de devise et les options d'affichage des quotas",
     "Configure custom OAuth providers for user authentication": "Configurer des fournisseurs OAuth personnalisés pour l'authentification des utilisateurs",
     "Configure custom OAuth providers for user authentication": "Configurer des fournisseurs OAuth personnalisés pour l'authentification des utilisateurs",
     "Configure daily check-in rewards for users": "Configurer les récompenses de connexion quotidienne pour les utilisateurs",
     "Configure daily check-in rewards for users": "Configurer les récompenses de connexion quotidienne pour les utilisateurs",
     "Configure discount rates based on recharge amounts": "Configurer les taux de réduction basés sur les montants de recharge",
     "Configure discount rates based on recharge amounts": "Configurer les taux de réduction basés sur les montants de recharge",
     "Configure experimental data export for the dashboard": "Configurer l'exportation de données expérimentales pour le tableau de bord",
     "Configure experimental data export for the dashboard": "Configurer l'exportation de données expérimentales pour le tableau de bord",
     "Configure Gemini safety behavior, version overrides, and thinking adapter": "Configurer le comportement de sécurité Gemini, les remplacements de version et l'adaptateur de réflexion",
     "Configure Gemini safety behavior, version overrides, and thinking adapter": "Configurer le comportement de sécurité Gemini, les remplacements de version et l'adaptateur de réflexion",
+    "Configure group ratios and group-specific pricing rules": "Configurer les ratios de groupe et les règles de tarification propres aux groupes",
     "Configure in your Creem dashboard": "Configurez dans votre tableau de bord Creem",
     "Configure in your Creem dashboard": "Configurez dans votre tableau de bord Creem",
     "Configure io.net API key for model deployments": "Configurer la clé API io.net pour les déploiements de modèles",
     "Configure io.net API key for model deployments": "Configurer la clé API io.net pour les déploiements de modèles",
     "Configure keyword filtering for prompts and responses.": "Configurer le filtrage par mots-clés pour les invites et les réponses.",
     "Configure keyword filtering for prompts and responses.": "Configurer le filtrage par mots-clés pour les invites et les réponses.",
+    "Configure model deployment provider settings": "Configurer les paramètres du fournisseur de déploiement de modèles",
+    "Configure model pricing ratios and tool prices": "Configurer les ratios de tarification des modèles et les prix des outils",
     "Configure model, caching, and group ratios used for billing": "Configurer les ratios de modèle, de mise en cache et de groupe utilisés pour la facturation",
     "Configure model, caching, and group ratios used for billing": "Configurer les ratios de modèle, de mise en cache et de groupe utilisés pour la facturation",
     "Configure monitoring status page groups for the dashboard": "Configurer les groupes de pages d'état de surveillance pour le tableau de bord",
     "Configure monitoring status page groups for the dashboard": "Configurer les groupes de pages d'état de surveillance pour le tableau de bord",
     "Configure outgoing email server for notifications": "Configurer le serveur de messagerie sortant pour les notifications",
     "Configure outgoing email server for notifications": "Configurer le serveur de messagerie sortant pour les notifications",
@@ -849,6 +855,7 @@
     "Console": "Console",
     "Console": "Console",
     "Console area": "Zone de console",
     "Console area": "Zone de console",
     "Console Area": "Zone console",
     "Console Area": "Zone console",
+    "Console Content": "Contenu de la console",
     "Consume": "Consommation",
     "Consume": "Consommation",
     "Container": "Conteneur",
     "Container": "Conteneur",
     "Container name": "Nom du conteneur",
     "Container name": "Nom du conteneur",
@@ -970,6 +977,7 @@
     "Cross-group retry": "Nouvelle tentative inter-groupes",
     "Cross-group retry": "Nouvelle tentative inter-groupes",
     "Curate quick links to your different Domains": "Organiser des liens rapides vers vos différents domaines",
     "Curate quick links to your different Domains": "Organiser des liens rapides vers vos différents domaines",
     "Currency": "Devise",
     "Currency": "Devise",
+    "Currency & Display": "Devise et affichage",
     "Current Balance": "Solde actuel",
     "Current Balance": "Solde actuel",
     "Current Billing": "Facturation actuelle",
     "Current Billing": "Facturation actuelle",
     "Current Cache Size": "Taille actuelle du cache",
     "Current Cache Size": "Taille actuelle du cache",
@@ -1027,6 +1035,7 @@
     "Date and time when this announcement should be displayed": "Date et heure auxquelles cette annonce doit être affichée",
     "Date and time when this announcement should be displayed": "Date et heure auxquelles cette annonce doit être affichée",
     "Date Range": "Plage de dates",
     "Date Range": "Plage de dates",
     "Day": "Jour",
     "Day": "Jour",
+    "Day of month": "Jour du mois",
     "days": "jours",
     "days": "jours",
     "Days to Retain": "Jours à conserver",
     "Days to Retain": "Jours à conserver",
     "Deducted by subscription": "Déduit par abonnement",
     "Deducted by subscription": "Déduit par abonnement",
@@ -1778,6 +1787,8 @@
     "GPU count": "Nombre de GPU",
     "GPU count": "Nombre de GPU",
     "Greater Than": "Supérieur à",
     "Greater Than": "Supérieur à",
     "Greater Than or Equal": "Supérieur ou égal",
     "Greater Than or Equal": "Supérieur ou égal",
+    "Greater than": "Supérieur à",
+    "Greater than or equal": "Supérieur ou égal",
     "Grok": "Grok",
     "Grok": "Grok",
     "Grok Settings": "Paramètres Grok",
     "Grok Settings": "Paramètres Grok",
     "Group": "Groupe",
     "Group": "Groupe",
@@ -1793,6 +1804,7 @@
     "Group Name": "Nom du groupe",
     "Group Name": "Nom du groupe",
     "Group name cannot be changed when editing.": "Le nom du groupe ne peut pas être modifié lors de la modification.",
     "Group name cannot be changed when editing.": "Le nom du groupe ne peut pas être modifié lors de la modification.",
     "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.": "Les prix par groupe ne peuvent pas être détaillés car cette expression n'est pas une expression tarifaire par paliers standard.",
     "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.": "Les prix par groupe ne peuvent pas être détaillés car cette expression n'est pas une expression tarifaire par paliers standard.",
+    "Group Pricing": "Tarification des groupes",
     "group ratio": "ratio de groupe",
     "group ratio": "ratio de groupe",
     "Group Ratio": "Ratio de groupe",
     "Group Ratio": "Ratio de groupe",
     "Group ratios": "Ratios de groupe",
     "Group ratios": "Ratios de groupe",
@@ -1848,6 +1860,7 @@
     "Home Page Content": "Contenu de la page d'accueil",
     "Home Page Content": "Contenu de la page d'accueil",
     "Hostname or IP of your SMTP provider": "Nom d'hôte ou IP de votre fournisseur SMTP",
     "Hostname or IP of your SMTP provider": "Nom d'hôte ou IP de votre fournisseur SMTP",
     "Hour": "Heure",
     "Hour": "Heure",
+    "Hour of day": "Heure du jour",
     "Hourly token usage by model across the last 24 hours": "Utilisation horaire des tokens par modèle sur les dernières 24 heures",
     "Hourly token usage by model across the last 24 hours": "Utilisation horaire des tokens par modèle sur les dernières 24 heures",
     "Hourly token usage by model over the past 24 hours": "Utilisation horaire de tokens par modèle sur les dernières 24 heures",
     "Hourly token usage by model over the past 24 hours": "Utilisation horaire de tokens par modèle sur les dernières 24 heures",
     "hours": "heures",
     "hours": "heures",
@@ -1893,6 +1906,7 @@
     "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "Si une erreur en amont contient l'un de ces mots-clés (insensible à la casse), le canal sera désactivé automatiquement.",
     "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "Si une erreur en amont contient l'un de ces mots-clés (insensible à la casse), le canal sera désactivé automatiquement.",
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "Si l'autorisation réussit, le JSON généré sera inséré dans le champ clé. Vous devez encore enregistrer le canal pour le conserver.",
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "Si l'autorisation réussit, le JSON généré sera inséré dans le champ clé. Vous devez encore enregistrer le canal pour le conserver.",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "Si vous vous connectez à des projets de relais One API ou New API en amont, utilisez le type OpenAI à la place sauf si vous savez ce que vous faites",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "Si vous vous connectez à des projets de relais One API ou New API en amont, utilisez le type OpenAI à la place sauf si vous savez ce que vous faites",
+    "If this keeps happening, please report it on GitHub Issues.": "Si cela continue, veuillez le signaler sur GitHub Issues.",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Si le canal affinitaire échoue et qu'une nouvelle tentative réussit sur un autre canal, mettre à jour l'affinité vers le canal ayant réussi.",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Si le canal affinitaire échoue et qu'une nouvelle tentative réussit sur un autre canal, mettre à jour l'affinité vers le canal ayant réussi.",
     "Ignored upstream models": "Modèles amont ignorés",
     "Ignored upstream models": "Modèles amont ignorés",
     "Image": "Image",
     "Image": "Image",
@@ -2056,6 +2070,8 @@
     "Less": "Moins",
     "Less": "Moins",
     "Less Than": "Inférieur à",
     "Less Than": "Inférieur à",
     "Less Than or Equal": "Inférieur ou égal",
     "Less Than or Equal": "Inférieur ou égal",
+    "Less than": "Inférieur à",
+    "Less than or equal": "Inférieur ou égal",
     "License": "Licence",
     "License": "Licence",
     "Light": "Clair",
     "Light": "Clair",
     "Lightning Fast": "Extrêmement rapide",
     "Lightning Fast": "Extrêmement rapide",
@@ -2217,6 +2233,7 @@
     "Model context usage": "Utilisation du contexte du modèle",
     "Model context usage": "Utilisation du contexte du modèle",
     "Model deleted": "Modèle supprimé",
     "Model deleted": "Modèle supprimé",
     "Model deleted successfully": "Modèle supprimé avec succès",
     "Model deleted successfully": "Modèle supprimé avec succès",
+    "Model Deployment": "Déploiement de modèles",
     "Model deployment service is disabled": "Le service de déploiement de modèles est désactivé",
     "Model deployment service is disabled": "Le service de déploiement de modèles est désactivé",
     "Model Description": "Description du modèle",
     "Model Description": "Description du modèle",
     "Model details": "Détails du modèle",
     "Model details": "Détails du modèle",
@@ -2256,6 +2273,7 @@
     "Models": "Modèles",
     "Models": "Modèles",
     "Models *": "Modèles *",
     "Models *": "Modèles *",
     "Models & Groups": "Modèles & Groupes",
     "Models & Groups": "Modèles & Groupes",
+    "Models & Routing": "Modèles et routage",
     "Models appended successfully": "Modèles ajoutés avec succès",
     "Models appended successfully": "Modèles ajoutés avec succès",
     "Models are required": "Les modèles sont requis",
     "Models are required": "Les modèles sont requis",
     "Models climbing the leaderboard": "Modèles qui montent au classement",
     "Models climbing the leaderboard": "Modèles qui montent au classement",
@@ -2276,6 +2294,7 @@
     "Monitor": "Surveiller",
     "Monitor": "Surveiller",
     "Monitoring & Alerts": "Surveillance & Alertes",
     "Monitoring & Alerts": "Surveillance & Alertes",
     "Month": "Mois",
     "Month": "Mois",
+    "Month number": "Numéro du mois",
     "Monthly": "Mensuel",
     "Monthly": "Mensuel",
     "Monthly tokens": "Tokens par mois",
     "Monthly tokens": "Tokens par mois",
     "months": "mois",
     "months": "mois",
@@ -2592,6 +2611,7 @@
     "Operation": "Opération",
     "Operation": "Opération",
     "Operation failed": "Opération échouée",
     "Operation failed": "Opération échouée",
     "Operation Type": "Type d'opération",
     "Operation Type": "Type d'opération",
+    "Operations": "Opérations",
     "Operator Admin": "Admin opérateur",
     "Operator Admin": "Admin opérateur",
     "Optimize system for self-hosted single-user usage": "Optimiser le système pour une utilisation auto-hébergée par un seul utilisateur",
     "Optimize system for self-hosted single-user usage": "Optimiser le système pour une utilisation auto-hébergée par un seul utilisateur",
     "Optimized network architecture ensures millisecond response times": "Architecture réseau optimisée garantissant des temps de réponse en millisecondes",
     "Optimized network architecture ensures millisecond response times": "Architecture réseau optimisée garantissant des temps de réponse en millisecondes",
@@ -2636,6 +2656,7 @@
     "Override Rules": "Règles de remplacement",
     "Override Rules": "Règles de remplacement",
     "Override the endpoint used for testing. Leave empty to auto detect.": "Remplacer le point de terminaison utilisé pour les tests. Laisser vide pour la détection automatique.",
     "Override the endpoint used for testing. Leave empty to auto detect.": "Remplacer le point de terminaison utilisé pour les tests. Laisser vide pour la détection automatique.",
     "overrides for matching model prefix.": "remplace le tarif si le modèle a ce préfixe.",
     "overrides for matching model prefix.": "remplace le tarif si le modèle a ce préfixe.",
+    "Overnight range": "Plage nocturne",
     "Overview": "Vue d'ensemble",
     "Overview": "Vue d'ensemble",
     "Overwritten": "Écrasé",
     "Overwritten": "Écrasé",
     "Page": "Page",
     "Page": "Page",
@@ -3102,6 +3123,7 @@
     "Replacement Model": "Modèle de remplacement",
     "Replacement Model": "Modèle de remplacement",
     "Replica count": "Nombre de réplicas",
     "Replica count": "Nombre de réplicas",
     "Replicate": "Replicate",
     "Replicate": "Replicate",
+    "Report an issue": "Signaler un problème",
     "request": "requête",
     "request": "requête",
     "Request": "Requête",
     "Request": "Requête",
     "Request Body Disk Cache": "Cache disque du corps de requête",
     "Request Body Disk Cache": "Cache disque du corps de requête",
@@ -3308,6 +3330,7 @@
     "Secret Key": "Clé secrète",
     "Secret Key": "Clé secrète",
     "Secure & Reliable": "Sécurisé et fiable",
     "Secure & Reliable": "Sécurisé et fiable",
     "Security": "Sécurité",
     "Security": "Sécurité",
+    "Security & Limits": "Sécurité et limites",
     "Security Check": "Vérification de sécurité",
     "Security Check": "Vérification de sécurité",
     "Security verification": "Vérification de sécurité",
     "Security verification": "Vérification de sécurité",
     "Select": "Sélectionner",
     "Select": "Sélectionner",
@@ -3456,6 +3479,7 @@
     "Simple mode only returns message; status code and error type use system defaults.": "Le mode simple ne retourne que le message ; le code de statut et le type d'erreur utilisent les valeurs par défaut.",
     "Simple mode only returns message; status code and error type use system defaults.": "Le mode simple ne retourne que le message ; le code de statut et le type d'erreur utilisent les valeurs par défaut.",
     "Simple mode: prune objects by type, e.g. redacted_thinking.": "Mode simple : nettoyer les objets par type, ex. redacted_thinking.",
     "Simple mode: prune objects by type, e.g. redacted_thinking.": "Mode simple : nettoyer les objets par type, ex. redacted_thinking.",
     "Single Key": "Clé unique",
     "Single Key": "Clé unique",
+    "Site & Branding": "Site et marque",
     "Site Key": "Clé du site",
     "Site Key": "Clé du site",
     "Size:": "Taille :",
     "Size:": "Taille :",
     "sk_xxx or rk_xxx": "sk_xxx ou rk_xxx",
     "sk_xxx or rk_xxx": "sk_xxx ou rk_xxx",
@@ -3733,6 +3757,8 @@
     "Tier": "Palier",
     "Tier": "Palier",
     "Tier name": "Nom du palier",
     "Tier name": "Nom du palier",
     "Tiered": "Par paliers",
     "Tiered": "Par paliers",
+    "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.",
     "Tiered (billing expression)": "Par paliers (expression de facturation)",
     "Tiered (billing expression)": "Par paliers (expression de facturation)",
     "Tiered price table": "Grille de prix par paliers",
     "Tiered price table": "Grille de prix par paliers",
     "Time": "Heure",
     "Time": "Heure",
@@ -3741,7 +3767,7 @@
     "Time window for rate limiting": "Fenêtre de temps pour la limitation de débit",
     "Time window for rate limiting": "Fenêtre de temps pour la limitation de débit",
     "Time-based": "Selon l’heure",
     "Time-based": "Selon l’heure",
     "Time:": "Heure :",
     "Time:": "Heure :",
-    "Timed cache (1h)": "Cache limité (1h)",
+    "Time-sliced cache (Claude)": "Cache segmenté (Claude)",
     "Timeline": "Chronologie",
     "Timeline": "Chronologie",
     "times": "Fois",
     "times": "Fois",
     "Timing": "Durée",
     "Timing": "Durée",
@@ -4219,6 +4245,71 @@
     "Zero retention": "Aucune rétention",
     "Zero retention": "Aucune rétention",
     "Zhipu": "Zhipu",
     "Zhipu": "Zhipu",
     "Zhipu V4": "Zhipu V4",
     "Zhipu V4": "Zhipu V4",
-    "Zoom": "Zoom"
+    "Zoom": "Zoom",
+    "Add model pricing": "Ajouter une tarification de modèle",
+    "Applied {{name}} pricing to {{count}} models": "Tarification de {{name}} appliquée à {{count}} modèles",
+    "Audio input price": "Prix d’entrée audio",
+    "Audio output price": "Prix de sortie audio",
+    "Audio output price requires an audio input price.": "Le prix de sortie audio nécessite un prix d’entrée audio.",
+    "Billable input tokens": "Tokens d’entrée facturables",
+    "Billable output tokens": "Tokens de sortie facturables",
+    "Cache create (1h) price": "Prix de création du cache (1 h)",
+    "Cache create price": "Prix de création du cache",
+    "Cache read price": "Prix de lecture du cache",
+    "Cache pricing": "Tarification du cache",
+    "Cache write price": "Prix d’écriture du cache",
+    "Changes are written to the settings draft on save.": "Les modifications sont écrites dans le brouillon des paramètres lors de l’enregistrement.",
+    "Clean": "Sans conflit",
+    "Completion price": "Prix de complétion",
+    "Core pricing": "Tarification principale",
+    "Copy {{name}} pricing": "Copier la tarification de {{name}}",
+    "Disabled lanes are omitted on save.": "Les voies désactivées sont omises à l’enregistrement.",
+    "Edit model pricing": "Modifier la tarification du modèle",
+    "Empty": "Vide",
+    "Each tier supports up to 2 conditions; the last tier is the catch-all without conditions. Use full input length for tier conditions to avoid mis-routing when cache hits reduce billable input tokens.": "Chaque palier accepte jusqu’à 2 conditions ; le dernier palier sert de repli sans condition. Utilisez la longueur complète de l’entrée pour éviter un mauvais aiguillage lorsque les lectures de cache réduisent les tokens d’entrée facturables.",
+    "All conditions must match before this tier is used.": "Toutes les conditions doivent correspondre pour utiliser ce palier.",
+    "Base input and output token prices for this tier.": "Prix de base des tokens d’entrée et de sortie pour ce palier.",
+    "Expression": "Expression",
+    "Fallback tier": "Palier de repli",
+    "Full input length": "Longueur complète de l’entrée",
+    "Input price is required before saving dependent prices.": "Le prix d’entrée est requis avant d’enregistrer les prix dépendants.",
+    "Image input price": "Prix d’entrée image",
+    "Image output price": "Prix de sortie image",
+    "Media pricing": "Tarification multimodale",
+    "New model": "Nouveau modèle",
+    "No separate media pricing configured.": "Aucune tarification multimodale séparée n’est configurée.",
+    "Open a source model first": "Ouvrez d’abord un modèle source",
+    "Output token price for generated tokens.": "Prix des tokens de sortie générés.",
+    "Per-request": "Par requête",
+    "Save preview": "Aperçu de l’enregistrement",
+    "Select at least one target model": "Sélectionnez au moins un modèle cible",
+    "Separate image/audio prices are enabled.": "Les prix séparés image/audio sont activés.",
+    "Set separate prices for cache reads and writes.": "Définissez des prix séparés pour les lectures et écritures du cache.",
+    "This model has both fixed-price and ratio settings. Saving the current mode will rewrite the conflicting fields.": "Ce modèle possède à la fois un prix fixe et des paramètres de ratio. L’enregistrement du mode actuel réécrira les champs en conflit.",
+    "This model has both fixed-price and token-price settings. Saving the current mode will rewrite the conflicting fields.": "Ce modèle possède à la fois un prix fixe et des prix par token. L’enregistrement du mode actuel réécrira les champs en conflit.",
+    "This tier catches any request that did not match earlier tiers.": "Ce palier récupère toute requête qui n’a pas correspondu aux paliers précédents.",
+    "Tier conditions": "Conditions du palier",
+    "Token price for audio input.": "Prix par token pour l’entrée audio.",
+    "Token price for audio output.": "Prix par token pour la sortie audio.",
+    "Token price for cache reads.": "Prix par token pour les lectures du cache.",
+    "Token price for creating cache entries.": "Prix par token pour la création d’entrées de cache.",
+    "Token price for image input.": "Prix par token pour l’entrée image.",
+    "USD price per 1M input tokens.": "Prix en USD par million de tokens d’entrée.",
+    "USD price per 1M tokens.": "Prix en USD par million de tokens.",
+    "{{count}} selected targets available for bulk copy.": "{{count}} cibles sélectionnées disponibles pour la copie en lot.",
+    "Base input price only": "Prix d’entrée de base uniquement",
+    "Expression based": "Basé sur une expression",
+    "Expression pricing": "Tarification par expression",
+    "Fixed request price": "Prix fixe par requête",
+    "Includes request rules": "Inclut des règles de requête",
+    "No base input price": "Aucun prix d’entrée de base",
+    "No models configured. Use Add model to get started.": "Aucun modèle configuré. Utilisez Ajouter un modèle pour commencer.",
+    "Price summary": "Résumé des prix",
+    "Select a model to edit pricing": "Sélectionnez un modèle pour modifier sa tarification",
+    "Tiered pricing": "Tarification par paliers",
+    "Unset price": "Prix non défini",
+    "Use the full-width table to scan prices, then select a row to edit it here.": "Parcourez les prix dans le tableau, puis sélectionnez une ligne pour la modifier ici.",
+    "extras": "suppléments",
+    "tiers": "paliers"
   }
   }
 }
 }

+ 93 - 2
web/default/src/i18n/locales/ja.json

@@ -494,6 +494,7 @@
     "Best for single-tenant deployments. Pricing and billing options stay hidden.": "シングルテナント環境に最適です。料金設定や請求オプションは非表示になります。",
     "Best for single-tenant deployments. Pricing and billing options stay hidden.": "シングルテナント環境に最適です。料金設定や請求オプションは非表示になります。",
     "Best TTFT": "最良 TTFT",
     "Best TTFT": "最良 TTFT",
     "Billing": "請求",
     "Billing": "請求",
+    "Billing & Payment": "請求と支払い",
     "Billing currency": "請求通貨",
     "Billing currency": "請求通貨",
     "Billing Details": "課金詳細",
     "Billing Details": "課金詳細",
     "Billing History": "請求履歴",
     "Billing History": "請求履歴",
@@ -633,6 +634,7 @@
     "Check in now": "今すぐチェックイン",
     "Check in now": "今すぐチェックイン",
     "Check resolved IPs against IP filters even when accessing by domain": "ドメインによるアクセスであっても、解決されたIPをIPフィルターと照合してチェックします",
     "Check resolved IPs against IP filters even when accessing by domain": "ドメインによるアクセスであっても、解決されたIPをIPフィルターと照合してチェックします",
     "Check-in failed": "チェックインできませんでした",
     "Check-in failed": "チェックインできませんでした",
+    "Check-in Rewards": "チェックイン報酬",
     "Check-in Settings": "チェックイン設定",
     "Check-in Settings": "チェックイン設定",
     "Check-in successful! Received": "チェックインに成功しました!受け取りました",
     "Check-in successful! Received": "チェックインに成功しました!受け取りました",
     "Checked in": "チェックイン済み",
     "Checked in": "チェックイン済み",
@@ -783,14 +785,18 @@
     "Configure basic system information and branding": "基本的なシステム情報とブランディングを設定",
     "Configure basic system information and branding": "基本的なシステム情報とブランディングを設定",
     "Configure channel affinity (sticky routing) rules": "チャネルアフィニティ(スティッキールーティング)ルールの設定",
     "Configure channel affinity (sticky routing) rules": "チャネルアフィニティ(スティッキールーティング)ルールの設定",
     "Configure Creem products. Provide a JSON array.": "Creem製品を設定。JSON配列を提供してください。",
     "Configure Creem products. Provide a JSON array.": "Creem製品を設定。JSON配列を提供してください。",
+    "Configure currency conversion and quota display options": "通貨換算とクォータ表示オプションを設定します",
     "Configure custom OAuth providers for user authentication": "ユーザー認証のためのカスタムOAuthプロバイダーを設定",
     "Configure custom OAuth providers for user authentication": "ユーザー認証のためのカスタムOAuthプロバイダーを設定",
     "Configure daily check-in rewards for users": "ユーザーの毎日のチェックイン報酬を設定する",
     "Configure daily check-in rewards for users": "ユーザーの毎日のチェックイン報酬を設定する",
     "Configure discount rates based on recharge amounts": "チャージ金額に基づいた割引率を設定",
     "Configure discount rates based on recharge amounts": "チャージ金額に基づいた割引率を設定",
     "Configure experimental data export for the dashboard": "ダッシュボード用の実験的なデータエクスポートを設定",
     "Configure experimental data export for the dashboard": "ダッシュボード用の実験的なデータエクスポートを設定",
     "Configure Gemini safety behavior, version overrides, and thinking adapter": "Geminiの安全動作、バージョン上書き、および思考アダプターを設定",
     "Configure Gemini safety behavior, version overrides, and thinking adapter": "Geminiの安全動作、バージョン上書き、および思考アダプターを設定",
+    "Configure group ratios and group-specific pricing rules": "グループ倍率とグループ固有の料金ルールを設定します",
     "Configure in your Creem dashboard": "Creem ダッシュボードで設定",
     "Configure in your Creem dashboard": "Creem ダッシュボードで設定",
     "Configure io.net API key for model deployments": "モデルデプロイ用の io.net API キーを設定します",
     "Configure io.net API key for model deployments": "モデルデプロイ用の io.net API キーを設定します",
     "Configure keyword filtering for prompts and responses.": "プロンプトと応答のキーワードフィルタリングを設定します。",
     "Configure keyword filtering for prompts and responses.": "プロンプトと応答のキーワードフィルタリングを設定します。",
+    "Configure model deployment provider settings": "モデルデプロイプロバイダー設定を構成します",
+    "Configure model pricing ratios and tool prices": "モデル料金倍率とツール料金を設定します",
     "Configure model, caching, and group ratios used for billing": "請求に使用されるモデル、キャッシュ、およびグループ比率を設定します。",
     "Configure model, caching, and group ratios used for billing": "請求に使用されるモデル、キャッシュ、およびグループ比率を設定します。",
     "Configure monitoring status page groups for the dashboard": "ダッシュボードの監視ステータスページグループを設定します。",
     "Configure monitoring status page groups for the dashboard": "ダッシュボードの監視ステータスページグループを設定します。",
     "Configure outgoing email server for notifications": "通知用の送信メールサーバーを設定します。",
     "Configure outgoing email server for notifications": "通知用の送信メールサーバーを設定します。",
@@ -849,6 +855,7 @@
     "Console": "コンソール",
     "Console": "コンソール",
     "Console area": "コンソールエリア",
     "Console area": "コンソールエリア",
     "Console Area": "コンソールエリア",
     "Console Area": "コンソールエリア",
+    "Console Content": "コンソールコンテンツ",
     "Consume": "消費",
     "Consume": "消費",
     "Container": "コンテナ",
     "Container": "コンテナ",
     "Container name": "コンテナ名",
     "Container name": "コンテナ名",
@@ -970,6 +977,7 @@
     "Cross-group retry": "グループ横断リトライ",
     "Cross-group retry": "グループ横断リトライ",
     "Curate quick links to your different Domains": "異なるドメインへのクイックリンクを厳選します",
     "Curate quick links to your different Domains": "異なるドメインへのクイックリンクを厳選します",
     "Currency": "通貨",
     "Currency": "通貨",
+    "Currency & Display": "通貨と表示",
     "Current Balance": "現在の残高",
     "Current Balance": "現在の残高",
     "Current Billing": "現在の請求",
     "Current Billing": "現在の請求",
     "Current Cache Size": "現在のキャッシュサイズ",
     "Current Cache Size": "現在のキャッシュサイズ",
@@ -1027,6 +1035,7 @@
     "Date and time when this announcement should be displayed": "このアナウンスが表示されるべき日時",
     "Date and time when this announcement should be displayed": "このアナウンスが表示されるべき日時",
     "Date Range": "期間",
     "Date Range": "期間",
     "Day": "日",
     "Day": "日",
+    "Day of month": "月内の日",
     "days": "日",
     "days": "日",
     "Days to Retain": "保持日数",
     "Days to Retain": "保持日数",
     "Deducted by subscription": "サブスクリプションで控除",
     "Deducted by subscription": "サブスクリプションで控除",
@@ -1778,6 +1787,8 @@
     "GPU count": "GPU 数",
     "GPU count": "GPU 数",
     "Greater Than": "より大きい",
     "Greater Than": "より大きい",
     "Greater Than or Equal": "以上",
     "Greater Than or Equal": "以上",
+    "Greater than": "より大きい",
+    "Greater than or equal": "以上",
     "Grok": "Grok",
     "Grok": "Grok",
     "Grok Settings": "Grok 設定",
     "Grok Settings": "Grok 設定",
     "Group": "グループ",
     "Group": "グループ",
@@ -1793,6 +1804,7 @@
     "Group Name": "グループ名",
     "Group Name": "グループ名",
     "Group name cannot be changed when editing.": "編集時はグループ名を変更できません。",
     "Group name cannot be changed when editing.": "編集時はグループ名を変更できません。",
     "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.": "この式は標準の段階制料金式ではないため、グループ別価格を展開できません。",
     "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.": "この式は標準の段階制料金式ではないため、グループ別価格を展開できません。",
+    "Group Pricing": "グループ料金",
     "group ratio": "グループ倍率",
     "group ratio": "グループ倍率",
     "Group Ratio": "グループ倍率",
     "Group Ratio": "グループ倍率",
     "Group ratios": "グループ比率",
     "Group ratios": "グループ比率",
@@ -1848,6 +1860,7 @@
     "Home Page Content": "ホームコンテンツ",
     "Home Page Content": "ホームコンテンツ",
     "Hostname or IP of your SMTP provider": "SMTP プロバイダーのホスト名または IP",
     "Hostname or IP of your SMTP provider": "SMTP プロバイダーのホスト名または IP",
     "Hour": "時間",
     "Hour": "時間",
+    "Hour of day": "時刻(時)",
     "Hourly token usage by model across the last 24 hours": "過去 24 時間にわたるモデル別の時間ごとのトークン使用量",
     "Hourly token usage by model across the last 24 hours": "過去 24 時間にわたるモデル別の時間ごとのトークン使用量",
     "Hourly token usage by model over the past 24 hours": "過去24時間のモデル別時間単位のトークン使用量",
     "Hourly token usage by model over the past 24 hours": "過去24時間のモデル別時間単位のトークン使用量",
     "hours": "時間",
     "hours": "時間",
@@ -1893,6 +1906,7 @@
     "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "アップストリームエラーにこれらのキーワードのいずれかが含まれている場合 (大文字と小文字を区別しない)、チャネルは自動的に無効になります。",
     "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "アップストリームエラーにこれらのキーワードのいずれかが含まれている場合 (大文字と小文字を区別しない)、チャネルは自動的に無効になります。",
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "認証が成功すると、生成されたJSONがキー欄に挿入されます。保存するにはチャンネルを保存してください。",
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "認証が成功すると、生成されたJSONがキー欄に挿入されます。保存するにはチャンネルを保存してください。",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "上流の One API または New API リレープロジェクトに接続する場合、知っている場合を除き OpenAI タイプを使用してください",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "上流の One API または New API リレープロジェクトに接続する場合、知っている場合を除き OpenAI タイプを使用してください",
+    "If this keeps happening, please report it on GitHub Issues.": "この問題が続く場合は、GitHub Issues で報告してください。",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "アフィニティチャネルが失敗し、別のチャネルでリトライが成功した場合、アフィニティを成功したチャネルに更新します。",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "アフィニティチャネルが失敗し、別のチャネルでリトライが成功した場合、アフィニティを成功したチャネルに更新します。",
     "Ignored upstream models": "無視する上流モデル",
     "Ignored upstream models": "無視する上流モデル",
     "Image": "画像",
     "Image": "画像",
@@ -2056,6 +2070,8 @@
     "Less": "少ない",
     "Less": "少ない",
     "Less Than": "より小さい",
     "Less Than": "より小さい",
     "Less Than or Equal": "以下",
     "Less Than or Equal": "以下",
+    "Less than": "より小さい",
+    "Less than or equal": "以下",
     "License": "ライセンス",
     "License": "ライセンス",
     "Light": "ライト",
     "Light": "ライト",
     "Lightning Fast": "超高速",
     "Lightning Fast": "超高速",
@@ -2217,6 +2233,7 @@
     "Model context usage": "モデルのコンテキスト使用量",
     "Model context usage": "モデルのコンテキスト使用量",
     "Model deleted": "モデルが削除されました",
     "Model deleted": "モデルが削除されました",
     "Model deleted successfully": "モデルが正常に削除されました",
     "Model deleted successfully": "モデルが正常に削除されました",
+    "Model Deployment": "モデルデプロイ",
     "Model deployment service is disabled": "モデルデプロイサービスが無効です",
     "Model deployment service is disabled": "モデルデプロイサービスが無効です",
     "Model Description": "モデルの説明",
     "Model Description": "モデルの説明",
     "Model details": "モデル詳細",
     "Model details": "モデル詳細",
@@ -2256,6 +2273,7 @@
     "Models": "モデル",
     "Models": "モデル",
     "Models *": "モデル *",
     "Models *": "モデル *",
     "Models & Groups": "モデルとグループ",
     "Models & Groups": "モデルとグループ",
+    "Models & Routing": "モデルとルーティング",
     "Models appended successfully": "モデルが正常に追加されました",
     "Models appended successfully": "モデルが正常に追加されました",
     "Models are required": "モデルが必要です",
     "Models are required": "モデルが必要です",
     "Models climbing the leaderboard": "ランキング上昇中のモデル",
     "Models climbing the leaderboard": "ランキング上昇中のモデル",
@@ -2276,6 +2294,7 @@
     "Monitor": "モニタリング",
     "Monitor": "モニタリング",
     "Monitoring & Alerts": "監視とアラート",
     "Monitoring & Alerts": "監視とアラート",
     "Month": "月",
     "Month": "月",
+    "Month number": "月番号",
     "Monthly": "毎月",
     "Monthly": "毎月",
     "Monthly tokens": "月間トークン",
     "Monthly tokens": "月間トークン",
     "months": "ヶ月",
     "months": "ヶ月",
@@ -2592,6 +2611,7 @@
     "Operation": "操作",
     "Operation": "操作",
     "Operation failed": "操作に失敗しました",
     "Operation failed": "操作に失敗しました",
     "Operation Type": "操作タイプ",
     "Operation Type": "操作タイプ",
+    "Operations": "運用",
     "Operator Admin": "オペレーター管理",
     "Operator Admin": "オペレーター管理",
     "Optimize system for self-hosted single-user usage": "セルフホスト型の単一ユーザー使用向けにシステムを最適化する",
     "Optimize system for self-hosted single-user usage": "セルフホスト型の単一ユーザー使用向けにシステムを最適化する",
     "Optimized network architecture ensures millisecond response times": "最適化されたネットワークアーキテクチャによりミリ秒単位の応答時間を保証",
     "Optimized network architecture ensures millisecond response times": "最適化されたネットワークアーキテクチャによりミリ秒単位の応答時間を保証",
@@ -2636,6 +2656,7 @@
     "Override Rules": "上書きルール",
     "Override Rules": "上書きルール",
     "Override the endpoint used for testing. Leave empty to auto detect.": "テストに使用されるエンドポイントを上書きします。自動検出するには空のままにします。",
     "Override the endpoint used for testing. Leave empty to auto detect.": "テストに使用されるエンドポイントを上書きします。自動検出するには空のままにします。",
     "overrides for matching model prefix.": "は一致するモデル接頭辞に上書きします。",
     "overrides for matching model prefix.": "は一致するモデル接頭辞に上書きします。",
+    "Overnight range": "日跨ぎ範囲",
     "Overview": "概要",
     "Overview": "概要",
     "Overwritten": "上書き済み",
     "Overwritten": "上書き済み",
     "Page": "ページ",
     "Page": "ページ",
@@ -3102,6 +3123,7 @@
     "Replacement Model": "代替モデル",
     "Replacement Model": "代替モデル",
     "Replica count": "レプリカ数",
     "Replica count": "レプリカ数",
     "Replicate": "Replicate",
     "Replicate": "Replicate",
+    "Report an issue": "問題を報告",
     "request": "リクエスト",
     "request": "リクエスト",
     "Request": "リクエスト",
     "Request": "リクエスト",
     "Request Body Disk Cache": "リクエストボディのディスクキャッシュ",
     "Request Body Disk Cache": "リクエストボディのディスクキャッシュ",
@@ -3308,6 +3330,7 @@
     "Secret Key": "シークレットキー",
     "Secret Key": "シークレットキー",
     "Secure & Reliable": "セキュア&信頼性",
     "Secure & Reliable": "セキュア&信頼性",
     "Security": "セキュリティ",
     "Security": "セキュリティ",
+    "Security & Limits": "セキュリティと制限",
     "Security Check": "セキュリティチェック",
     "Security Check": "セキュリティチェック",
     "Security verification": "セキュリティ確認",
     "Security verification": "セキュリティ確認",
     "Select": "選択",
     "Select": "選択",
@@ -3456,6 +3479,7 @@
     "Simple mode only returns message; status code and error type use system defaults.": "シンプルモードはメッセージのみ返します。ステータスコードとエラータイプはシステムデフォルトを使用します。",
     "Simple mode only returns message; status code and error type use system defaults.": "シンプルモードはメッセージのみ返します。ステータスコードとエラータイプはシステムデフォルトを使用します。",
     "Simple mode: prune objects by type, e.g. redacted_thinking.": "シンプルモード:typeでオブジェクトを削除(例:redacted_thinking)。",
     "Simple mode: prune objects by type, e.g. redacted_thinking.": "シンプルモード:typeでオブジェクトを削除(例:redacted_thinking)。",
     "Single Key": "単一キー",
     "Single Key": "単一キー",
+    "Site & Branding": "サイトとブランド",
     "Site Key": "サイトキー",
     "Site Key": "サイトキー",
     "Size:": "サイズ:",
     "Size:": "サイズ:",
     "sk_xxx or rk_xxx": "sk_xxx または rk_xxx",
     "sk_xxx or rk_xxx": "sk_xxx または rk_xxx",
@@ -3733,6 +3757,8 @@
     "Tier": "ティア",
     "Tier": "ティア",
     "Tier name": "ティア名",
     "Tier name": "ティア名",
     "Tiered": "段階的",
     "Tiered": "段階的",
+    "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.",
     "Tiered (billing expression)": "段階的(課金式)",
     "Tiered (billing expression)": "段階的(課金式)",
     "Tiered price table": "段階別価格表",
     "Tiered price table": "段階別価格表",
     "Time": "時間",
     "Time": "時間",
@@ -3741,7 +3767,7 @@
     "Time window for rate limiting": "レート制限の時間枠",
     "Time window for rate limiting": "レート制限の時間枠",
     "Time-based": "時間条件あり",
     "Time-based": "時間条件あり",
     "Time:": "時間:",
     "Time:": "時間:",
-    "Timed cache (1h)": "有効期限付きキャッシュ(1h)",
+    "Time-sliced cache (Claude)": "時間分割キャッシュ(Claude)",
     "Timeline": "タイムライン",
     "Timeline": "タイムライン",
     "times": "回",
     "times": "回",
     "Timing": "所要時間",
     "Timing": "所要時間",
@@ -4219,6 +4245,71 @@
     "Zero retention": "データ保持なし",
     "Zero retention": "データ保持なし",
     "Zhipu": "Zhipu",
     "Zhipu": "Zhipu",
     "Zhipu V4": "Zhipu V 4",
     "Zhipu V4": "Zhipu V 4",
-    "Zoom": "ズーム"
+    "Zoom": "ズーム",
+    "Add model pricing": "モデル料金を追加",
+    "Applied {{name}} pricing to {{count}} models": "{{name}} の料金を {{count}} 個のモデルに適用しました",
+    "Audio input price": "音声入力価格",
+    "Audio output price": "音声出力価格",
+    "Audio output price requires an audio input price.": "音声出力価格には音声入力価格が必要です。",
+    "Billable input tokens": "課金対象の入力トークン",
+    "Billable output tokens": "課金対象の出力トークン",
+    "Cache create (1h) price": "キャッシュ作成価格 (1時間)",
+    "Cache create price": "キャッシュ作成価格",
+    "Cache read price": "キャッシュ読み取り価格",
+    "Cache pricing": "キャッシュ価格",
+    "Cache write price": "キャッシュ書き込み価格",
+    "Changes are written to the settings draft on save.": "保存すると変更は設定ドラフトに書き込まれます。",
+    "Clean": "問題なし",
+    "Completion price": "補完価格",
+    "Core pricing": "基本価格",
+    "Copy {{name}} pricing": "{{name}} の料金をコピー",
+    "Disabled lanes are omitted on save.": "無効な価格レーンは保存時に省略されます。",
+    "Edit model pricing": "モデル料金を編集",
+    "Empty": "空",
+    "Each tier supports up to 2 conditions; the last tier is the catch-all without conditions. Use full input length for tier conditions to avoid mis-routing when cache hits reduce billable input tokens.": "各階層は最大2つの条件をサポートします。最後の階層は条件なしのフォールバックです。キャッシュヒットで課金対象の入力トークンが減っても誤った階層にならないよう、条件には完全な入力長を使用してください。",
+    "All conditions must match before this tier is used.": "この階層を使用するには、すべての条件に一致する必要があります。",
+    "Base input and output token prices for this tier.": "この階層の基本入力・出力トークン価格。",
+    "Expression": "式",
+    "Fallback tier": "フォールバック階層",
+    "Full input length": "完全な入力長",
+    "Input price is required before saving dependent prices.": "依存する価格を保存する前に入力価格が必要です。",
+    "Image input price": "画像入力価格",
+    "Image output price": "画像出力価格",
+    "Media pricing": "メディア価格",
+    "New model": "新しいモデル",
+    "No separate media pricing configured.": "個別のメディア価格は設定されていません。",
+    "Open a source model first": "先にソースモデルを開いてください",
+    "Output token price for generated tokens.": "生成された出力トークンの価格。",
+    "Per-request": "リクエスト単位",
+    "Save preview": "保存プレビュー",
+    "Select at least one target model": "少なくとも1つの対象モデルを選択してください",
+    "Separate image/audio prices are enabled.": "画像/音声の個別価格が有効です。",
+    "Set separate prices for cache reads and writes.": "キャッシュ読み取りと書き込みに個別価格を設定します。",
+    "This model has both fixed-price and ratio settings. Saving the current mode will rewrite the conflicting fields.": "このモデルには固定価格と比率設定の両方があります。現在のモードで保存すると、競合するフィールドが上書きされます。",
+    "This model has both fixed-price and token-price settings. Saving the current mode will rewrite the conflicting fields.": "このモデルには固定価格とトークン価格設定の両方があります。現在のモードで保存すると、競合するフィールドが上書きされます。",
+    "This tier catches any request that did not match earlier tiers.": "この階層は、前の階層に一致しなかったすべてのリクエストを受けます。",
+    "Tier conditions": "階層条件",
+    "Token price for audio input.": "音声入力のトークン価格。",
+    "Token price for audio output.": "音声出力のトークン価格。",
+    "Token price for cache reads.": "キャッシュ読み取りのトークン価格。",
+    "Token price for creating cache entries.": "キャッシュ作成のトークン価格。",
+    "Token price for image input.": "画像入力のトークン価格。",
+    "USD price per 1M input tokens.": "100万入力トークンあたりのUSD価格。",
+    "USD price per 1M tokens.": "100万トークンあたりのUSD価格。",
+    "{{count}} selected targets available for bulk copy.": "一括コピーに使用できる対象が {{count}} 個選択されています。",
+    "Base input price only": "基本入力価格のみ",
+    "Expression based": "式ベース",
+    "Expression pricing": "式による料金",
+    "Fixed request price": "固定リクエスト価格",
+    "Includes request rules": "リクエストルールを含む",
+    "No base input price": "基本入力価格なし",
+    "No models configured. Use Add model to get started.": "モデルが設定されていません。モデルを追加して開始してください。",
+    "Price summary": "価格概要",
+    "Select a model to edit pricing": "料金を編集するモデルを選択",
+    "Tiered pricing": "階層料金",
+    "Unset price": "価格未設定",
+    "Use the full-width table to scan prices, then select a row to edit it here.": "表で価格を確認し、行を選択してここで編集します。",
+    "extras": "追加項目",
+    "tiers": "階層"
   }
   }
 }
 }

+ 94 - 3
web/default/src/i18n/locales/ru.json

@@ -494,6 +494,7 @@
     "Best for single-tenant deployments. Pricing and billing options stay hidden.": "Лучший вариант для однопользовательских развёртываний. Опции ценообразования и биллинга будут скрыты.",
     "Best for single-tenant deployments. Pricing and billing options stay hidden.": "Лучший вариант для однопользовательских развёртываний. Опции ценообразования и биллинга будут скрыты.",
     "Best TTFT": "Лучший TTFT",
     "Best TTFT": "Лучший TTFT",
     "Billing": "Биллинг",
     "Billing": "Биллинг",
+    "Billing & Payment": "Биллинг и платежи",
     "Billing currency": "Валюта оплаты",
     "Billing currency": "Валюта оплаты",
     "Billing Details": "Детали биллинга",
     "Billing Details": "Детали биллинга",
     "Billing History": "История биллинга",
     "Billing History": "История биллинга",
@@ -633,6 +634,7 @@
     "Check in now": "Войдите сейчас",
     "Check in now": "Войдите сейчас",
     "Check resolved IPs against IP filters even when accessing by domain": "Проверять разрешенные IP-адреса по IP-фильтрам даже при доступе по домену",
     "Check resolved IPs against IP filters even when accessing by domain": "Проверять разрешенные IP-адреса по IP-фильтрам даже при доступе по домену",
     "Check-in failed": "Регистрация не удалась.",
     "Check-in failed": "Регистрация не удалась.",
+    "Check-in Rewards": "Награды за отметку",
     "Check-in Settings": "Настройки прибытия",
     "Check-in Settings": "Настройки прибытия",
     "Check-in successful! Received": "Прибытие прошло успешно! Получено",
     "Check-in successful! Received": "Прибытие прошло успешно! Получено",
     "Checked in": "Зарегистрирован",
     "Checked in": "Зарегистрирован",
@@ -783,14 +785,18 @@
     "Configure basic system information and branding": "Настроить основную информацию о системе и брендинг",
     "Configure basic system information and branding": "Настроить основную информацию о системе и брендинг",
     "Configure channel affinity (sticky routing) rules": "Настроить правила привязки к каналу (липкая маршрутизация)",
     "Configure channel affinity (sticky routing) rules": "Настроить правила привязки к каналу (липкая маршрутизация)",
     "Configure Creem products. Provide a JSON array.": "Настройте продукты Creem. Укажите массив JSON.",
     "Configure Creem products. Provide a JSON array.": "Настройте продукты Creem. Укажите массив JSON.",
+    "Configure currency conversion and quota display options": "Настройте конвертацию валюты и параметры отображения квот",
     "Configure custom OAuth providers for user authentication": "Настройка пользовательских OAuth-провайдеров для аутентификации пользователей",
     "Configure custom OAuth providers for user authentication": "Настройка пользовательских OAuth-провайдеров для аутентификации пользователей",
     "Configure daily check-in rewards for users": "Настроить ежедневные награды за регистрацию для пользователей",
     "Configure daily check-in rewards for users": "Настроить ежедневные награды за регистрацию для пользователей",
     "Configure discount rates based on recharge amounts": "Настроить скидки в зависимости от сумм пополнения",
     "Configure discount rates based on recharge amounts": "Настроить скидки в зависимости от сумм пополнения",
     "Configure experimental data export for the dashboard": "Настроить экспериментальный экспорт данных для панели управления",
     "Configure experimental data export for the dashboard": "Настроить экспериментальный экспорт данных для панели управления",
     "Configure Gemini safety behavior, version overrides, and thinking adapter": "Настроить поведение безопасности Gemini, переопределения версий и адаптер мышления",
     "Configure Gemini safety behavior, version overrides, and thinking adapter": "Настроить поведение безопасности Gemini, переопределения версий и адаптер мышления",
+    "Configure group ratios and group-specific pricing rules": "Настройте коэффициенты групп и правила тарификации для групп",
     "Configure in your Creem dashboard": "Настройте в панели управления Creem",
     "Configure in your Creem dashboard": "Настройте в панели управления Creem",
     "Configure io.net API key for model deployments": "Настройте API-ключ io.net для развертывания моделей",
     "Configure io.net API key for model deployments": "Настройте API-ключ io.net для развертывания моделей",
     "Configure keyword filtering for prompts and responses.": "Настроить фильтрацию по ключевым словам для запросов и ответов.",
     "Configure keyword filtering for prompts and responses.": "Настроить фильтрацию по ключевым словам для запросов и ответов.",
+    "Configure model deployment provider settings": "Настройте параметры провайдера развертывания моделей",
+    "Configure model pricing ratios and tool prices": "Настройте коэффициенты тарификации моделей и цены инструментов",
     "Configure model, caching, and group ratios used for billing": "Настроить модель, кэширование и групповые коэффициенты, используемые для выставления счетов",
     "Configure model, caching, and group ratios used for billing": "Настроить модель, кэширование и групповые коэффициенты, используемые для выставления счетов",
     "Configure monitoring status page groups for the dashboard": "Настроить группы страниц состояния мониторинга для панели управления",
     "Configure monitoring status page groups for the dashboard": "Настроить группы страниц состояния мониторинга для панели управления",
     "Configure outgoing email server for notifications": "Настроить исходящий почтовый сервер для уведомлений",
     "Configure outgoing email server for notifications": "Настроить исходящий почтовый сервер для уведомлений",
@@ -849,6 +855,7 @@
     "Console": "Консоль",
     "Console": "Консоль",
     "Console area": "Область консоли",
     "Console area": "Область консоли",
     "Console Area": "Область консоли",
     "Console Area": "Область консоли",
+    "Console Content": "Содержимое консоли",
     "Consume": "Расход",
     "Consume": "Расход",
     "Container": "Контейнер",
     "Container": "Контейнер",
     "Container name": "Имя контейнера",
     "Container name": "Имя контейнера",
@@ -970,6 +977,7 @@
     "Cross-group retry": "Повтор между группами",
     "Cross-group retry": "Повтор между группами",
     "Curate quick links to your different Domains": "Подбирайте быстрые ссылки на ваши различные домены",
     "Curate quick links to your different Domains": "Подбирайте быстрые ссылки на ваши различные домены",
     "Currency": "Валюта",
     "Currency": "Валюта",
+    "Currency & Display": "Валюта и отображение",
     "Current Balance": "Текущий баланс",
     "Current Balance": "Текущий баланс",
     "Current Billing": "Текущие счета",
     "Current Billing": "Текущие счета",
     "Current Cache Size": "Текущий размер кэша",
     "Current Cache Size": "Текущий размер кэша",
@@ -1027,6 +1035,7 @@
     "Date and time when this announcement should be displayed": "Дата и время отображения этого объявления",
     "Date and time when this announcement should be displayed": "Дата и время отображения этого объявления",
     "Date Range": "Диапазон дат",
     "Date Range": "Диапазон дат",
     "Day": "День",
     "Day": "День",
+    "Day of month": "День месяца",
     "days": "дней",
     "days": "дней",
     "Days to Retain": "Дней хранения",
     "Days to Retain": "Дней хранения",
     "Deducted by subscription": "Списано по подписке",
     "Deducted by subscription": "Списано по подписке",
@@ -1778,6 +1787,8 @@
     "GPU count": "Количество GPU",
     "GPU count": "Количество GPU",
     "Greater Than": "Больше",
     "Greater Than": "Больше",
     "Greater Than or Equal": "Больше или равно",
     "Greater Than or Equal": "Больше или равно",
+    "Greater than": "Больше",
+    "Greater than or equal": "Больше или равно",
     "Grok": "Grok",
     "Grok": "Grok",
     "Grok Settings": "Настройки Grok",
     "Grok Settings": "Настройки Grok",
     "Group": "Группа",
     "Group": "Группа",
@@ -1793,6 +1804,7 @@
     "Group Name": "Имя группы",
     "Group Name": "Имя группы",
     "Group name cannot be changed when editing.": "Имя группы нельзя изменить при редактировании.",
     "Group name cannot be changed when editing.": "Имя группы нельзя изменить при редактировании.",
     "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.": "Цены по группам нельзя развернуть, потому что это не стандартное выражение тарифов по уровням.",
     "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.": "Цены по группам нельзя развернуть, потому что это не стандартное выражение тарифов по уровням.",
+    "Group Pricing": "Тарификация групп",
     "group ratio": "коэффициент группы",
     "group ratio": "коэффициент группы",
     "Group Ratio": "Групповой коэффициент",
     "Group Ratio": "Групповой коэффициент",
     "Group ratios": "Соотношения группы",
     "Group ratios": "Соотношения группы",
@@ -1848,6 +1860,7 @@
     "Home Page Content": "Содержимое главной страницы",
     "Home Page Content": "Содержимое главной страницы",
     "Hostname or IP of your SMTP provider": "Имя хоста или IP-адрес вашего SMTP-провайдера",
     "Hostname or IP of your SMTP provider": "Имя хоста или IP-адрес вашего SMTP-провайдера",
     "Hour": "Час",
     "Hour": "Час",
+    "Hour of day": "Час суток",
     "Hourly token usage by model across the last 24 hours": "Почасовое использование токенов по моделям за последние 24 часа",
     "Hourly token usage by model across the last 24 hours": "Почасовое использование токенов по моделям за последние 24 часа",
     "Hourly token usage by model over the past 24 hours": "Почасовое использование токенов по моделям за последние 24 часа",
     "Hourly token usage by model over the past 24 hours": "Почасовое использование токенов по моделям за последние 24 часа",
     "hours": "часов",
     "hours": "часов",
@@ -1893,6 +1906,7 @@
     "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "Если ошибка вышестоящего уровня содержит любое из этих ключевых слов (без учета регистра), канал будет автоматически отключен.",
     "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "Если ошибка вышестоящего уровня содержит любое из этих ключевых слов (без учета регистра), канал будет автоматически отключен.",
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "При успешной авторизации сгенерированный JSON будет вставлен в поле ключа. Сохраните канал, чтобы применить изменения.",
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "При успешной авторизации сгенерированный JSON будет вставлен в поле ключа. Сохраните канал, чтобы применить изменения.",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "При подключении к upstream One API или проектам-ретрансляторам New API используйте тип OpenAI, если только вы точно знаете, что делаете",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "При подключении к upstream One API или проектам-ретрансляторам New API используйте тип OpenAI, если только вы точно знаете, что делаете",
+    "If this keeps happening, please report it on GitHub Issues.": "Если проблема повторяется, сообщите о ней в GitHub Issues.",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Если привязанный канал не работает и повторная попытка удалась через другой канал, привязка обновляется на успешный канал.",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Если привязанный канал не работает и повторная попытка удалась через другой канал, привязка обновляется на успешный канал.",
     "Ignored upstream models": "Игнорируемые upstream-модели",
     "Ignored upstream models": "Игнорируемые upstream-модели",
     "Image": "Изображение",
     "Image": "Изображение",
@@ -2056,6 +2070,8 @@
     "Less": "Меньше",
     "Less": "Меньше",
     "Less Than": "Меньше",
     "Less Than": "Меньше",
     "Less Than or Equal": "Меньше или равно",
     "Less Than or Equal": "Меньше или равно",
+    "Less than": "Меньше",
+    "Less than or equal": "Меньше или равно",
     "License": "Лицензия",
     "License": "Лицензия",
     "Light": "Светлая",
     "Light": "Светлая",
     "Lightning Fast": "Молниеносно быстро",
     "Lightning Fast": "Молниеносно быстро",
@@ -2217,6 +2233,7 @@
     "Model context usage": "Использование контекста модели",
     "Model context usage": "Использование контекста модели",
     "Model deleted": "Модель удалена",
     "Model deleted": "Модель удалена",
     "Model deleted successfully": "Модель успешно удалена",
     "Model deleted successfully": "Модель успешно удалена",
+    "Model Deployment": "Развертывание моделей",
     "Model deployment service is disabled": "Сервис развертывания моделей отключен",
     "Model deployment service is disabled": "Сервис развертывания моделей отключен",
     "Model Description": "Описание модели",
     "Model Description": "Описание модели",
     "Model details": "Сведения о модели",
     "Model details": "Сведения о модели",
@@ -2238,7 +2255,7 @@
     "Model performance metrics": "Метрики производительности моделей",
     "Model performance metrics": "Метрики производительности моделей",
     "Model Price": "Цена модели",
     "Model Price": "Цена модели",
     "Model Price Not Configured": "Цена модели не настроена",
     "Model Price Not Configured": "Цена модели не настроена",
-    "Model Pricing": "Цены на модели",
+    "Model Pricing": "Тарификация моделей",
     "Model pull failed: {{msg}}": "Ошибка тяги модели: {{msg}}",
     "Model pull failed: {{msg}}": "Ошибка тяги модели: {{msg}}",
     "Model ratio": "Коэффициент модели",
     "Model ratio": "Коэффициент модели",
     "Model ratios": "Коэффициенты модели",
     "Model ratios": "Коэффициенты модели",
@@ -2256,6 +2273,7 @@
     "Models": "Модели",
     "Models": "Модели",
     "Models *": "Модели *",
     "Models *": "Модели *",
     "Models & Groups": "Модели и группы",
     "Models & Groups": "Модели и группы",
+    "Models & Routing": "Модели и маршрутизация",
     "Models appended successfully": "Модели успешно добавлены",
     "Models appended successfully": "Модели успешно добавлены",
     "Models are required": "Требуются модели",
     "Models are required": "Требуются модели",
     "Models climbing the leaderboard": "Модели, поднимающиеся в рейтинге",
     "Models climbing the leaderboard": "Модели, поднимающиеся в рейтинге",
@@ -2276,6 +2294,7 @@
     "Monitor": "Мониторинг",
     "Monitor": "Мониторинг",
     "Monitoring & Alerts": "Мониторинг и оповещения",
     "Monitoring & Alerts": "Мониторинг и оповещения",
     "Month": "Месяц",
     "Month": "Месяц",
+    "Month number": "Номер месяца",
     "Monthly": "Ежемесячно",
     "Monthly": "Ежемесячно",
     "Monthly tokens": "Токенов в месяц",
     "Monthly tokens": "Токенов в месяц",
     "months": "месяцев",
     "months": "месяцев",
@@ -2592,6 +2611,7 @@
     "Operation": "Операция",
     "Operation": "Операция",
     "Operation failed": "Операция не удалась",
     "Operation failed": "Операция не удалась",
     "Operation Type": "Тип операции",
     "Operation Type": "Тип операции",
+    "Operations": "Операции",
     "Operator Admin": "Оператор-администратор",
     "Operator Admin": "Оператор-администратор",
     "Optimize system for self-hosted single-user usage": "Оптимизировать систему для самостоятельного использования одним пользователем",
     "Optimize system for self-hosted single-user usage": "Оптимизировать систему для самостоятельного использования одним пользователем",
     "Optimized network architecture ensures millisecond response times": "Оптимизированная сетевая архитектура обеспечивает время отклика в миллисекунды",
     "Optimized network architecture ensures millisecond response times": "Оптимизированная сетевая архитектура обеспечивает время отклика в миллисекунды",
@@ -2636,6 +2656,7 @@
     "Override Rules": "Правила переопределения",
     "Override Rules": "Правила переопределения",
     "Override the endpoint used for testing. Leave empty to auto detect.": "Переопределить конечную точку, используемую для тестирования. Оставьте пустым для автоматического определения.",
     "Override the endpoint used for testing. Leave empty to auto detect.": "Переопределить конечную точку, используемую для тестирования. Оставьте пустым для автоматического определения.",
     "overrides for matching model prefix.": "переопределяет цену по совпавшему префиксу модели.",
     "overrides for matching model prefix.": "переопределяет цену по совпавшему префиксу модели.",
+    "Overnight range": "Диапазон через полночь",
     "Overview": "Обзор",
     "Overview": "Обзор",
     "Overwritten": "Перезаписано",
     "Overwritten": "Перезаписано",
     "Page": "Страница",
     "Page": "Страница",
@@ -3102,6 +3123,7 @@
     "Replacement Model": "Модель замены",
     "Replacement Model": "Модель замены",
     "Replica count": "Количество реплик",
     "Replica count": "Количество реплик",
     "Replicate": "Replicate",
     "Replicate": "Replicate",
+    "Report an issue": "Сообщить о проблеме",
     "request": "запрос",
     "request": "запрос",
     "Request": "Запрос",
     "Request": "Запрос",
     "Request Body Disk Cache": "Дисковый кэш тела запроса",
     "Request Body Disk Cache": "Дисковый кэш тела запроса",
@@ -3308,6 +3330,7 @@
     "Secret Key": "Секретный ключ",
     "Secret Key": "Секретный ключ",
     "Secure & Reliable": "Безопасно и надежно",
     "Secure & Reliable": "Безопасно и надежно",
     "Security": "Безопасность",
     "Security": "Безопасность",
+    "Security & Limits": "Безопасность и лимиты",
     "Security Check": "Проверка безопасности",
     "Security Check": "Проверка безопасности",
     "Security verification": "Подтверждение безопасности",
     "Security verification": "Подтверждение безопасности",
     "Select": "Выбрать",
     "Select": "Выбрать",
@@ -3456,6 +3479,7 @@
     "Simple mode only returns message; status code and error type use system defaults.": "Простой режим возвращает только сообщение; код статуса и тип ошибки используют системные значения по умолчанию.",
     "Simple mode only returns message; status code and error type use system defaults.": "Простой режим возвращает только сообщение; код статуса и тип ошибки используют системные значения по умолчанию.",
     "Simple mode: prune objects by type, e.g. redacted_thinking.": "Простой режим: очистка объектов по типу, например redacted_thinking.",
     "Simple mode: prune objects by type, e.g. redacted_thinking.": "Простой режим: очистка объектов по типу, например redacted_thinking.",
     "Single Key": "Одиночный ключ",
     "Single Key": "Одиночный ключ",
+    "Site & Branding": "Сайт и брендинг",
     "Site Key": "Ключ сайта",
     "Site Key": "Ключ сайта",
     "Size:": "Размер:",
     "Size:": "Размер:",
     "sk_xxx or rk_xxx": "sk_xxx или rk_xxx",
     "sk_xxx or rk_xxx": "sk_xxx или rk_xxx",
@@ -3733,6 +3757,8 @@
     "Tier": "Уровень",
     "Tier": "Уровень",
     "Tier name": "Название уровня",
     "Tier name": "Название уровня",
     "Tiered": "Ступенчато",
     "Tiered": "Ступенчато",
+    "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.",
     "Tiered (billing expression)": "Ступенчато (платёжное выражение)",
     "Tiered (billing expression)": "Ступенчато (платёжное выражение)",
     "Tiered price table": "Тариф по уровням",
     "Tiered price table": "Тариф по уровням",
     "Time": "Время",
     "Time": "Время",
@@ -3741,7 +3767,7 @@
     "Time window for rate limiting": "Временное окно для ограничения скорости запросов",
     "Time window for rate limiting": "Временное окно для ограничения скорости запросов",
     "Time-based": "Зависит от времени",
     "Time-based": "Зависит от времени",
     "Time:": "Время:",
     "Time:": "Время:",
-    "Timed cache (1h)": "Кэш с TTL (1 ч)",
+    "Time-sliced cache (Claude)": "Сегментированный кэш (Claude)",
     "Timeline": "Хронология",
     "Timeline": "Хронология",
     "times": "раз",
     "times": "раз",
     "Timing": "Время",
     "Timing": "Время",
@@ -4219,6 +4245,71 @@
     "Zero retention": "Без хранения данных",
     "Zero retention": "Без хранения данных",
     "Zhipu": "Zhipu",
     "Zhipu": "Zhipu",
     "Zhipu V4": "Zhipu V4",
     "Zhipu V4": "Zhipu V4",
-    "Zoom": "Zoom"
+    "Zoom": "Zoom",
+    "Add model pricing": "Добавить тариф модели",
+    "Applied {{name}} pricing to {{count}} models": "Тариф {{name}} применён к {{count}} моделям",
+    "Audio input price": "Цена аудиовхода",
+    "Audio output price": "Цена аудиовыхода",
+    "Audio output price requires an audio input price.": "Для цены аудиовыхода нужна цена аудиовхода.",
+    "Billable input tokens": "Оплачиваемые входные токены",
+    "Billable output tokens": "Оплачиваемые выходные токены",
+    "Cache create (1h) price": "Цена создания кэша (1 ч)",
+    "Cache create price": "Цена создания кэша",
+    "Cache read price": "Цена чтения кэша",
+    "Cache pricing": "Cache pricing",
+    "Cache write price": "Цена записи кэша",
+    "Changes are written to the settings draft on save.": "Изменения будут записаны в черновик настроек при сохранении.",
+    "Clean": "Без конфликта",
+    "Completion price": "Цена завершения",
+    "Core pricing": "Core pricing",
+    "Copy {{name}} pricing": "Копировать тариф {{name}}",
+    "Disabled lanes are omitted on save.": "Отключённые каналы цен не сохраняются.",
+    "Edit model pricing": "Изменить тариф модели",
+    "Empty": "Пусто",
+    "Each tier supports up to 2 conditions; the last tier is the catch-all without conditions. Use full input length for tier conditions to avoid mis-routing when cache hits reduce billable input tokens.": "Каждый уровень поддерживает до 2 условий; последний уровень является резервным и не содержит условий. Используйте полную длину входа для условий уровня, чтобы кэш-попадания не снижали оплачиваемые входные токены и не приводили к неверному маршруту.",
+    "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.",
+    "Expression": "Выражение",
+    "Fallback tier": "Fallback tier",
+    "Full input length": "Полная длина входа",
+    "Input price is required before saving dependent prices.": "Перед сохранением зависимых цен укажите входную цену.",
+    "Image input price": "Цена входного изображения",
+    "Image output price": "Цена выходного изображения",
+    "Media pricing": "Media pricing",
+    "New model": "Новая модель",
+    "No separate media pricing configured.": "No separate media pricing configured.",
+    "Open a source model first": "Сначала откройте исходную модель",
+    "Output token price for generated tokens.": "Цена выходных токенов для сгенерированного текста.",
+    "Per-request": "За запрос",
+    "Save preview": "Предпросмотр сохранения",
+    "Select at least one target model": "Выберите хотя бы одну целевую модель",
+    "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 model has both fixed-price and ratio settings. Saving the current mode will rewrite the conflicting fields.": "У этой модели одновременно заданы фиксированная цена и коэффициенты. Сохранение текущего режима перезапишет конфликтующие поля.",
+    "This model has both fixed-price and token-price settings. Saving the current mode will rewrite the conflicting fields.": "У этой модели одновременно заданы фиксированная цена и цены за токены. Сохранение текущего режима перезапишет конфликтующие поля.",
+    "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",
+    "Token price for audio input.": "Цена токенов для аудиовхода.",
+    "Token price for audio output.": "Цена токенов для аудиовыхода.",
+    "Token price for cache reads.": "Цена токенов для чтения кэша.",
+    "Token price for creating cache entries.": "Цена токенов для создания записей кэша.",
+    "Token price for image input.": "Цена токенов для входного изображения.",
+    "USD price per 1M input tokens.": "Цена в USD за 1 млн входных токенов.",
+    "USD price per 1M tokens.": "Цена в USD за 1 млн токенов.",
+    "{{count}} selected targets available for bulk copy.": "Для массового копирования выбрано целей: {{count}}.",
+    "Base input price only": "Только базовая цена входа",
+    "Expression based": "На основе выражения",
+    "Expression pricing": "Тарификация выражением",
+    "Fixed request price": "Фиксированная цена запроса",
+    "Includes request rules": "Включает правила запросов",
+    "No base input price": "Нет базовой цены входа",
+    "No models configured. Use Add model to get started.": "Модели не настроены. Используйте добавление модели, чтобы начать.",
+    "Price summary": "Сводка цен",
+    "Select a model to edit pricing": "Выберите модель для редактирования тарифа",
+    "Tiered pricing": "Многоуровневая тарификация",
+    "Unset price": "Цена не задана",
+    "Use the full-width table to scan prices, then select a row to edit it here.": "Просмотрите цены в таблице, затем выберите строку для редактирования здесь.",
+    "extras": "доп. пункты",
+    "tiers": "уровни"
   }
   }
 }
 }

+ 94 - 3
web/default/src/i18n/locales/vi.json

@@ -494,6 +494,7 @@
     "Best for single-tenant deployments. Pricing and billing options stay hidden.": "Phù hợp nhất cho triển khai đơn người dùng. Các tùy chọn giá và thanh toán sẽ được ẩn.",
     "Best for single-tenant deployments. Pricing and billing options stay hidden.": "Phù hợp nhất cho triển khai đơn người dùng. Các tùy chọn giá và thanh toán sẽ được ẩn.",
     "Best TTFT": "TTFT tốt nhất",
     "Best TTFT": "TTFT tốt nhất",
     "Billing": "Thanh toán",
     "Billing": "Thanh toán",
+    "Billing & Payment": "Thanh toán & chi phí",
     "Billing currency": "Loại tiền thanh toán",
     "Billing currency": "Loại tiền thanh toán",
     "Billing Details": "Chi tiết thanh toán",
     "Billing Details": "Chi tiết thanh toán",
     "Billing History": "Lịch sử thanh toán",
     "Billing History": "Lịch sử thanh toán",
@@ -633,6 +634,7 @@
     "Check in now": "Điểm danh ngay",
     "Check in now": "Điểm danh ngay",
     "Check resolved IPs against IP filters even when accessing by domain": "Kiểm tra các IP đã phân giải đối chiếu với các bộ lọc IP ngay cả khi truy cập bằng tên miền",
     "Check resolved IPs against IP filters even when accessing by domain": "Kiểm tra các IP đã phân giải đối chiếu với các bộ lọc IP ngay cả khi truy cập bằng tên miền",
     "Check-in failed": "Điểm danh thất bại",
     "Check-in failed": "Điểm danh thất bại",
+    "Check-in Rewards": "Phần thưởng điểm danh",
     "Check-in Settings": "Cài đặt điểm danh",
     "Check-in Settings": "Cài đặt điểm danh",
     "Check-in successful! Received": "Điểm danh thành công! Đã nhận",
     "Check-in successful! Received": "Điểm danh thành công! Đã nhận",
     "Checked in": "Đã check-in",
     "Checked in": "Đã check-in",
@@ -783,14 +785,18 @@
     "Configure basic system information and branding": "Cấu hình thông tin hệ thống cơ bản và nhận diện thương hiệu",
     "Configure basic system information and branding": "Cấu hình thông tin hệ thống cơ bản và nhận diện thương hiệu",
     "Configure channel affinity (sticky routing) rules": "Cấu hình quy tắc ưu tiên kênh (định tuyến dính)",
     "Configure channel affinity (sticky routing) rules": "Cấu hình quy tắc ưu tiên kênh (định tuyến dính)",
     "Configure Creem products. Provide a JSON array.": "Cấu hình sản phẩm Creem. Cung cấp một mảng JSON.",
     "Configure Creem products. Provide a JSON array.": "Cấu hình sản phẩm Creem. Cung cấp một mảng JSON.",
+    "Configure currency conversion and quota display options": "Cấu hình quy đổi tiền tệ và tùy chọn hiển thị hạn mức",
     "Configure custom OAuth providers for user authentication": "Cấu hình nhà cung cấp OAuth tùy chỉnh cho xác thực người dùng",
     "Configure custom OAuth providers for user authentication": "Cấu hình nhà cung cấp OAuth tùy chỉnh cho xác thực người dùng",
     "Configure daily check-in rewards for users": "Cấu hình phần thưởng điểm danh hàng ngày cho người dùng",
     "Configure daily check-in rewards for users": "Cấu hình phần thưởng điểm danh hàng ngày cho người dùng",
     "Configure discount rates based on recharge amounts": "Cấu hình tỷ lệ chiết khấu dựa trên số tiền nạp",
     "Configure discount rates based on recharge amounts": "Cấu hình tỷ lệ chiết khấu dựa trên số tiền nạp",
     "Configure experimental data export for the dashboard": "Cấu hình xuất dữ liệu thử nghiệm cho bảng điều khiển",
     "Configure experimental data export for the dashboard": "Cấu hình xuất dữ liệu thử nghiệm cho bảng điều khiển",
     "Configure Gemini safety behavior, version overrides, and thinking adapter": "Cấu hình hành vi an toàn Gemini, ghi đè phiên bản và bộ điều hợp tư duy",
     "Configure Gemini safety behavior, version overrides, and thinking adapter": "Cấu hình hành vi an toàn Gemini, ghi đè phiên bản và bộ điều hợp tư duy",
+    "Configure group ratios and group-specific pricing rules": "Cấu hình tỷ lệ nhóm và quy tắc định giá riêng theo nhóm",
     "Configure in your Creem dashboard": "Cấu hình trong bảng điều khiển Creem của bạn",
     "Configure in your Creem dashboard": "Cấu hình trong bảng điều khiển Creem của bạn",
     "Configure io.net API key for model deployments": "Cấu hình khóa API io.net cho triển khai mô hình",
     "Configure io.net API key for model deployments": "Cấu hình khóa API io.net cho triển khai mô hình",
     "Configure keyword filtering for prompts and responses.": "Định cấu hình lọc từ khóa để xem lời nhắc và câu trả lời.",
     "Configure keyword filtering for prompts and responses.": "Định cấu hình lọc từ khóa để xem lời nhắc và câu trả lời.",
+    "Configure model deployment provider settings": "Cấu hình nhà cung cấp triển khai mô hình",
+    "Configure model pricing ratios and tool prices": "Cấu hình tỷ lệ định giá mô hình và giá công cụ",
     "Configure model, caching, and group ratios used for billing": "Cấu hình mô hình, bộ nhớ đệm và tỷ lệ nhóm được sử dụng để tính phí.",
     "Configure model, caching, and group ratios used for billing": "Cấu hình mô hình, bộ nhớ đệm và tỷ lệ nhóm được sử dụng để tính phí.",
     "Configure monitoring status page groups for the dashboard": "Cấu hình các nhóm trang trạng thái giám sát cho bảng điều khiển",
     "Configure monitoring status page groups for the dashboard": "Cấu hình các nhóm trang trạng thái giám sát cho bảng điều khiển",
     "Configure outgoing email server for notifications": "Cấu hình máy chủ email gửi đi cho thông báo",
     "Configure outgoing email server for notifications": "Cấu hình máy chủ email gửi đi cho thông báo",
@@ -849,6 +855,7 @@
     "Console": "Bảng điều khiển",
     "Console": "Bảng điều khiển",
     "Console area": "Khu vực bảng điều khiển",
     "Console area": "Khu vực bảng điều khiển",
     "Console Area": "Khu vực bảng điều khiển",
     "Console Area": "Khu vực bảng điều khiển",
+    "Console Content": "Nội dung bảng điều khiển",
     "Consume": "Tiêu thụ",
     "Consume": "Tiêu thụ",
     "Container": "Thùng chứa",
     "Container": "Thùng chứa",
     "Container name": "Tên container",
     "Container name": "Tên container",
@@ -970,6 +977,7 @@
     "Cross-group retry": "Thử lại liên nhóm",
     "Cross-group retry": "Thử lại liên nhóm",
     "Curate quick links to your different Domains": "Sắp xếp các liên kết nhanh đến các Miền khác nhau của bạn",
     "Curate quick links to your different Domains": "Sắp xếp các liên kết nhanh đến các Miền khác nhau của bạn",
     "Currency": "Tiền tệ",
     "Currency": "Tiền tệ",
+    "Currency & Display": "Tiền tệ & hiển thị",
     "Current Balance": "Số Dư Hiện Tại",
     "Current Balance": "Số Dư Hiện Tại",
     "Current Billing": "Thanh toán hiện tại",
     "Current Billing": "Thanh toán hiện tại",
     "Current Cache Size": "Kích thước bộ nhớ đệm hiện tại",
     "Current Cache Size": "Kích thước bộ nhớ đệm hiện tại",
@@ -1027,6 +1035,7 @@
     "Date and time when this announcement should be displayed": "The date and time this notification should be displayed",
     "Date and time when this announcement should be displayed": "The date and time this notification should be displayed",
     "Date Range": "Khoảng thời gian",
     "Date Range": "Khoảng thời gian",
     "Day": "Ngày",
     "Day": "Ngày",
+    "Day of month": "Ngày trong tháng",
     "days": "ngày",
     "days": "ngày",
     "Days to Retain": "Số ngày giữ lại",
     "Days to Retain": "Số ngày giữ lại",
     "Deducted by subscription": "Khấu trừ bởi gói đăng ký",
     "Deducted by subscription": "Khấu trừ bởi gói đăng ký",
@@ -1778,6 +1787,8 @@
     "GPU count": "Số lượng GPU",
     "GPU count": "Số lượng GPU",
     "Greater Than": "Lớn hơn",
     "Greater Than": "Lớn hơn",
     "Greater Than or Equal": "Lớn hơn hoặc bằng",
     "Greater Than or Equal": "Lớn hơn hoặc bằng",
+    "Greater than": "Lớn hơn",
+    "Greater than or equal": "Lớn hơn hoặc bằng",
     "Grok": "Grok",
     "Grok": "Grok",
     "Grok Settings": "Cài đặt Grok",
     "Grok Settings": "Cài đặt Grok",
     "Group": "Nhóm",
     "Group": "Nhóm",
@@ -1793,6 +1804,7 @@
     "Group Name": "Tên Nhóm",
     "Group Name": "Tên Nhóm",
     "Group name cannot be changed when editing.": "Tên nhóm không thể thay đổi khi chỉnh sửa.",
     "Group name cannot be changed when editing.": "Tên nhóm không thể thay đổi khi chỉnh sửa.",
     "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.": "Không thể mở rộng giá theo nhóm vì biểu thức này không phải là biểu thức giá theo bậc tiêu chuẩn.",
     "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.": "Không thể mở rộng giá theo nhóm vì biểu thức này không phải là biểu thức giá theo bậc tiêu chuẩn.",
+    "Group Pricing": "Định giá nhóm",
     "group ratio": "tỷ lệ nhóm",
     "group ratio": "tỷ lệ nhóm",
     "Group Ratio": "Tỷ lệ nhóm",
     "Group Ratio": "Tỷ lệ nhóm",
     "Group ratios": "Tỷ lệ nhóm",
     "Group ratios": "Tỷ lệ nhóm",
@@ -1848,6 +1860,7 @@
     "Home Page Content": "Nội dung Trang chủ",
     "Home Page Content": "Nội dung Trang chủ",
     "Hostname or IP of your SMTP provider": "Tên máy chủ hoặc IP của nhà cung cấp SMTP của bạn",
     "Hostname or IP of your SMTP provider": "Tên máy chủ hoặc IP của nhà cung cấp SMTP của bạn",
     "Hour": "Giờ",
     "Hour": "Giờ",
+    "Hour of day": "Giờ trong ngày",
     "Hourly token usage by model across the last 24 hours": "Sử dụng token theo giờ của từng mô hình trong 24 giờ qua",
     "Hourly token usage by model across the last 24 hours": "Sử dụng token theo giờ của từng mô hình trong 24 giờ qua",
     "Hourly token usage by model over the past 24 hours": "Sử dụng token theo mô hình theo giờ trong 24 giờ qua",
     "Hourly token usage by model over the past 24 hours": "Sử dụng token theo mô hình theo giờ trong 24 giờ qua",
     "hours": "giờ",
     "hours": "giờ",
@@ -1893,6 +1906,7 @@
     "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "Nếu một lỗi thượng nguồn chứa bất kỳ từ khóa nào trong số này (không phân biệt chữ hoa chữ thường), kênh sẽ tự động bị vô hiệu hóa.",
     "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "Nếu một lỗi thượng nguồn chứa bất kỳ từ khóa nào trong số này (không phân biệt chữ hoa chữ thường), kênh sẽ tự động bị vô hiệu hóa.",
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "Nếu ủy quyền thành công, JSON tạo ra sẽ được chèn vào trường khóa. Bạn vẫn cần lưu kênh để áp dụng.",
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "Nếu ủy quyền thành công, JSON tạo ra sẽ được chèn vào trường khóa. Bạn vẫn cần lưu kênh để áp dụng.",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "Nếu kết nối với dự án relay One API hoặc New API upstream, hãy sử dụng loại OpenAI thay thế trừ khi bạn biết mình đang làm gì",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "Nếu kết nối với dự án relay One API hoặc New API upstream, hãy sử dụng loại OpenAI thay thế trừ khi bạn biết mình đang làm gì",
+    "If this keeps happening, please report it on GitHub Issues.": "Nếu sự cố tiếp tục xảy ra, vui lòng báo cáo trên GitHub Issues.",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Nếu kênh ưu tiên thất bại và thử lại thành công trên kênh khác, cập nhật ưu tiên sang kênh thành công.",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "Nếu kênh ưu tiên thất bại và thử lại thành công trên kênh khác, cập nhật ưu tiên sang kênh thành công.",
     "Ignored upstream models": "Mô hình upstream bị bỏ qua",
     "Ignored upstream models": "Mô hình upstream bị bỏ qua",
     "Image": "Hình ảnh",
     "Image": "Hình ảnh",
@@ -2056,6 +2070,8 @@
     "Less": "Ít hơn",
     "Less": "Ít hơn",
     "Less Than": "Nhỏ hơn",
     "Less Than": "Nhỏ hơn",
     "Less Than or Equal": "Nhỏ hơn hoặc bằng",
     "Less Than or Equal": "Nhỏ hơn hoặc bằng",
+    "Less than": "Nhỏ hơn",
+    "Less than or equal": "Nhỏ hơn hoặc bằng",
     "License": "Giấy phép",
     "License": "Giấy phép",
     "Light": "Ánh sáng",
     "Light": "Ánh sáng",
     "Lightning Fast": "Nhanh như chớp",
     "Lightning Fast": "Nhanh như chớp",
@@ -2217,6 +2233,7 @@
     "Model context usage": "Sử dụng ngữ cảnh mô hình",
     "Model context usage": "Sử dụng ngữ cảnh mô hình",
     "Model deleted": "Đã xóa mô hình",
     "Model deleted": "Đã xóa mô hình",
     "Model deleted successfully": "Model đã được xóa thành công",
     "Model deleted successfully": "Model đã được xóa thành công",
+    "Model Deployment": "Triển khai mô hình",
     "Model deployment service is disabled": "Dịch vụ triển khai mô hình đang bị tắt",
     "Model deployment service is disabled": "Dịch vụ triển khai mô hình đang bị tắt",
     "Model Description": "Mô tả Mô hình",
     "Model Description": "Mô tả Mô hình",
     "Model details": "Chi tiết mô hình",
     "Model details": "Chi tiết mô hình",
@@ -2238,7 +2255,7 @@
     "Model performance metrics": "Chỉ số hiệu năng mô hình",
     "Model performance metrics": "Chỉ số hiệu năng mô hình",
     "Model Price": "Giá mô hình",
     "Model Price": "Giá mô hình",
     "Model Price Not Configured": "Giá mô hình chưa được cấu hình",
     "Model Price Not Configured": "Giá mô hình chưa được cấu hình",
-    "Model Pricing": "Bảng giá mô hình",
+    "Model Pricing": "Định giá mô hình",
     "Model pull failed: {{msg}}": "Tải mô hình thất bại: {{msg}}",
     "Model pull failed: {{msg}}": "Tải mô hình thất bại: {{msg}}",
     "Model ratio": "Tỷ lệ mô hình",
     "Model ratio": "Tỷ lệ mô hình",
     "Model ratios": "Tỷ lệ mô hình",
     "Model ratios": "Tỷ lệ mô hình",
@@ -2256,6 +2273,7 @@
     "Models": "Mô hình",
     "Models": "Mô hình",
     "Models *": "Các mô hình *",
     "Models *": "Các mô hình *",
     "Models & Groups": "Mô hình & Nhóm",
     "Models & Groups": "Mô hình & Nhóm",
+    "Models & Routing": "Mô hình & định tuyến",
     "Models appended successfully": "Đã thêm mô hình thành công",
     "Models appended successfully": "Đã thêm mô hình thành công",
     "Models are required": "Các mô hình được yêu cầu",
     "Models are required": "Các mô hình được yêu cầu",
     "Models climbing the leaderboard": "Các mô hình đang leo bảng xếp hạng",
     "Models climbing the leaderboard": "Các mô hình đang leo bảng xếp hạng",
@@ -2276,6 +2294,7 @@
     "Monitor": "Giám sát",
     "Monitor": "Giám sát",
     "Monitoring & Alerts": "Giám sát & Cảnh báo",
     "Monitoring & Alerts": "Giám sát & Cảnh báo",
     "Month": "Tháng",
     "Month": "Tháng",
+    "Month number": "Số tháng",
     "Monthly": "Hàng tháng",
     "Monthly": "Hàng tháng",
     "Monthly tokens": "Token mỗi tháng",
     "Monthly tokens": "Token mỗi tháng",
     "months": "tháng",
     "months": "tháng",
@@ -2592,6 +2611,7 @@
     "Operation": "Thao tác",
     "Operation": "Thao tác",
     "Operation failed": "Thao tác thất bại",
     "Operation failed": "Thao tác thất bại",
     "Operation Type": "Loại thao tác",
     "Operation Type": "Loại thao tác",
+    "Operations": "Vận hành",
     "Operator Admin": "Quản trị viên vận hành",
     "Operator Admin": "Quản trị viên vận hành",
     "Optimize system for self-hosted single-user usage": "Tối ưu hóa hệ thống cho việc sử dụng đơn người dùng tự lưu trữ",
     "Optimize system for self-hosted single-user usage": "Tối ưu hóa hệ thống cho việc sử dụng đơn người dùng tự lưu trữ",
     "Optimized network architecture ensures millisecond response times": "Kiến trúc mạng tối ưu đảm bảo thời gian phản hồi mili giây",
     "Optimized network architecture ensures millisecond response times": "Kiến trúc mạng tối ưu đảm bảo thời gian phản hồi mili giây",
@@ -2636,6 +2656,7 @@
     "Override Rules": "Quy tắc ghi đè",
     "Override Rules": "Quy tắc ghi đè",
     "Override the endpoint used for testing. Leave empty to auto detect.": "Ghi đè điểm cuối dùng để kiểm thử. Để trống để tự động phát hiện.",
     "Override the endpoint used for testing. Leave empty to auto detect.": "Ghi đè điểm cuối dùng để kiểm thử. Để trống để tự động phát hiện.",
     "overrides for matching model prefix.": "ghi đè theo tiền tố model tương ứng.",
     "overrides for matching model prefix.": "ghi đè theo tiền tố model tương ứng.",
+    "Overnight range": "Khoảng qua nửa đêm",
     "Overview": "Tổng quan",
     "Overview": "Tổng quan",
     "Overwritten": "Đã ghi đè",
     "Overwritten": "Đã ghi đè",
     "Page": "Trang",
     "Page": "Trang",
@@ -3102,6 +3123,7 @@
     "Replacement Model": "Replacement model",
     "Replacement Model": "Replacement model",
     "Replica count": "Số bản sao",
     "Replica count": "Số bản sao",
     "Replicate": "Sao chép",
     "Replicate": "Sao chép",
+    "Report an issue": "Báo cáo sự cố",
     "request": "yêu cầu",
     "request": "yêu cầu",
     "Request": "Yêu cầu",
     "Request": "Yêu cầu",
     "Request Body Disk Cache": "Bộ nhớ đệm đĩa nội dung yêu cầu",
     "Request Body Disk Cache": "Bộ nhớ đệm đĩa nội dung yêu cầu",
@@ -3308,6 +3330,7 @@
     "Secret Key": "Khóa bí mật",
     "Secret Key": "Khóa bí mật",
     "Secure & Reliable": "An toàn & Đáng tin cậy",
     "Secure & Reliable": "An toàn & Đáng tin cậy",
     "Security": "Bảo mật",
     "Security": "Bảo mật",
+    "Security & Limits": "Bảo mật & giới hạn",
     "Security Check": "Kiểm tra bảo mật",
     "Security Check": "Kiểm tra bảo mật",
     "Security verification": "Xác minh bảo mật",
     "Security verification": "Xác minh bảo mật",
     "Select": "Chọn",
     "Select": "Chọn",
@@ -3456,6 +3479,7 @@
     "Simple mode only returns message; status code and error type use system defaults.": "Chế độ đơn giản chỉ trả về message; mã trạng thái và loại lỗi sử dụng giá trị mặc định.",
     "Simple mode only returns message; status code and error type use system defaults.": "Chế độ đơn giản chỉ trả về message; mã trạng thái và loại lỗi sử dụng giá trị mặc định.",
     "Simple mode: prune objects by type, e.g. redacted_thinking.": "Chế độ đơn giản: dọn dẹp đối tượng theo type, ví dụ redacted_thinking.",
     "Simple mode: prune objects by type, e.g. redacted_thinking.": "Chế độ đơn giản: dọn dẹp đối tượng theo type, ví dụ redacted_thinking.",
     "Single Key": "Khóa đơn",
     "Single Key": "Khóa đơn",
+    "Site & Branding": "Trang web & thương hiệu",
     "Site Key": "Khóa trang web",
     "Site Key": "Khóa trang web",
     "Size:": "Kích thước:",
     "Size:": "Kích thước:",
     "sk_xxx or rk_xxx": "sk_xxx hoặc rk_xxx",
     "sk_xxx or rk_xxx": "sk_xxx hoặc rk_xxx",
@@ -3733,6 +3757,8 @@
     "Tier": "Bậc",
     "Tier": "Bậc",
     "Tier name": "Tên bậc",
     "Tier name": "Tên bậc",
     "Tiered": "Nhiều bậc",
     "Tiered": "Nhiều bậc",
+    "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.",
     "Tiered (billing expression)": "Nhiều bậc (công thức tính phí)",
     "Tiered (billing expression)": "Nhiều bậc (công thức tính phí)",
     "Tiered price table": "Bảng giá theo bậc",
     "Tiered price table": "Bảng giá theo bậc",
     "Time": "Thời gian",
     "Time": "Thời gian",
@@ -3741,7 +3767,7 @@
     "Time window for rate limiting": "Cửa sổ thời gian cho giới hạn tốc độ",
     "Time window for rate limiting": "Cửa sổ thời gian cho giới hạn tốc độ",
     "Time-based": "Theo thời gian",
     "Time-based": "Theo thời gian",
     "Time:": "Thời gian:",
     "Time:": "Thời gian:",
-    "Timed cache (1h)": "Bộ đệm theo thời gian (1 giờ)",
+    "Time-sliced cache (Claude)": "Bộ đệm phân thời gian (Claude)",
     "Timeline": "Dòng thời gian",
     "Timeline": "Dòng thời gian",
     "times": "lần",
     "times": "lần",
     "Timing": "Thời gian",
     "Timing": "Thời gian",
@@ -4219,6 +4245,71 @@
     "Zero retention": "Không lưu dữ liệu",
     "Zero retention": "Không lưu dữ liệu",
     "Zhipu": "Zhipu",
     "Zhipu": "Zhipu",
     "Zhipu V4": "Zhipu V4",
     "Zhipu V4": "Zhipu V4",
-    "Zoom": "Zoom"
+    "Zoom": "Zoom",
+    "Add model pricing": "Thêm giá mô hình",
+    "Applied {{name}} pricing to {{count}} models": "Đã áp dụng giá của {{name}} cho {{count}} mô hình",
+    "Audio input price": "Giá đầu vào âm thanh",
+    "Audio output price": "Giá đầu ra âm thanh",
+    "Audio output price requires an audio input price.": "Giá đầu ra âm thanh cần có giá đầu vào âm thanh.",
+    "Billable input tokens": "Token đầu vào tính phí",
+    "Billable output tokens": "Token đầu ra tính phí",
+    "Cache create (1h) price": "Giá tạo cache (1 giờ)",
+    "Cache create price": "Giá tạo cache",
+    "Cache read price": "Giá đọc cache",
+    "Cache pricing": "Cache pricing",
+    "Cache write price": "Giá ghi cache",
+    "Changes are written to the settings draft on save.": "Các thay đổi sẽ được ghi vào bản nháp cài đặt khi lưu.",
+    "Clean": "Không xung đột",
+    "Completion price": "Giá hoàn thành",
+    "Core pricing": "Core pricing",
+    "Copy {{name}} pricing": "Sao chép giá của {{name}}",
+    "Disabled lanes are omitted on save.": "Các kênh bị tắt sẽ được bỏ qua khi lưu.",
+    "Edit model pricing": "Chỉnh sửa giá mô hình",
+    "Empty": "Trống",
+    "Each tier supports up to 2 conditions; the last tier is the catch-all without conditions. Use full input length for tier conditions to avoid mis-routing when cache hits reduce billable input tokens.": "Mỗi tầng hỗ trợ tối đa 2 điều kiện; tầng cuối cùng là tầng dự phòng không có điều kiện. Hãy dùng độ dài đầu vào đầy đủ cho điều kiện tầng để tránh chọn sai tầng khi cache hit làm giảm token đầu vào tính phí.",
+    "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.",
+    "Expression": "Biểu thức",
+    "Fallback tier": "Fallback tier",
+    "Full input length": "Độ dài đầu vào đầy đủ",
+    "Input price is required before saving dependent prices.": "Cần có giá đầu vào trước khi lưu các giá phụ thuộc.",
+    "Image input price": "Giá đầu vào hình ảnh",
+    "Image output price": "Giá đầu ra hình ảnh",
+    "Media pricing": "Media pricing",
+    "New model": "Mô hình mới",
+    "No separate media pricing configured.": "No separate media pricing configured.",
+    "Open a source model first": "Mở một mô hình nguồn trước",
+    "Output token price for generated tokens.": "Giá token đầu ra cho nội dung được tạo.",
+    "Per-request": "Theo yêu cầu",
+    "Save preview": "Xem trước lưu",
+    "Select at least one target model": "Chọn ít nhất một mô hình đích",
+    "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 model has both fixed-price and ratio settings. Saving the current mode will rewrite the conflicting fields.": "Mô hình này có cả giá cố định và cài đặt tỷ lệ. Lưu chế độ hiện tại sẽ ghi lại các trường xung đột.",
+    "This model has both fixed-price and token-price settings. Saving the current mode will rewrite the conflicting fields.": "Mô hình này có cả giá cố định và cài đặt giá theo token. Lưu chế độ hiện tại sẽ ghi lại các trường xung đột.",
+    "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",
+    "Token price for audio input.": "Giá token cho đầu vào âm thanh.",
+    "Token price for audio output.": "Giá token cho đầu ra âm thanh.",
+    "Token price for cache reads.": "Giá token cho lượt đọc cache.",
+    "Token price for creating cache entries.": "Giá token cho việc tạo mục cache.",
+    "Token price for image input.": "Giá token cho đầu vào hình ảnh.",
+    "USD price per 1M input tokens.": "Giá USD cho mỗi 1 triệu token đầu vào.",
+    "USD price per 1M tokens.": "Giá USD cho mỗi 1 triệu token.",
+    "{{count}} selected targets available for bulk copy.": "Có {{count}} mục tiêu đã chọn để sao chép hàng loạt.",
+    "Base input price only": "Chỉ có giá đầu vào cơ bản",
+    "Expression based": "Dựa trên biểu thức",
+    "Expression pricing": "Định giá bằng biểu thức",
+    "Fixed request price": "Giá cố định theo yêu cầu",
+    "Includes request rules": "Bao gồm quy tắc yêu cầu",
+    "No base input price": "Chưa có giá đầu vào cơ bản",
+    "No models configured. Use Add model to get started.": "Chưa cấu hình mô hình. Dùng Thêm mô hình để bắt đầu.",
+    "Price summary": "Tóm tắt giá",
+    "Select a model to edit pricing": "Chọn mô hình để chỉnh sửa giá",
+    "Tiered pricing": "Định giá theo tầng",
+    "Unset price": "Chưa đặt giá",
+    "Use the full-width table to scan prices, then select a row to edit it here.": "Duyệt giá trong bảng, rồi chọn một hàng để chỉnh sửa tại đây.",
+    "extras": "mục bổ sung",
+    "tiers": "tầng"
   }
   }
 }
 }

+ 93 - 2
web/default/src/i18n/locales/zh.json

@@ -494,6 +494,7 @@
     "Best for single-tenant deployments. Pricing and billing options stay hidden.": "适合单用户部署。定价和计费选项将被隐藏。",
     "Best for single-tenant deployments. Pricing and billing options stay hidden.": "适合单用户部署。定价和计费选项将被隐藏。",
     "Best TTFT": "最优 TTFT",
     "Best TTFT": "最优 TTFT",
     "Billing": "计费",
     "Billing": "计费",
+    "Billing & Payment": "计费与支付",
     "Billing currency": "计费货币",
     "Billing currency": "计费货币",
     "Billing Details": "计费详情",
     "Billing Details": "计费详情",
     "Billing History": "计费历史",
     "Billing History": "计费历史",
@@ -633,6 +634,7 @@
     "Check in now": "立即签到",
     "Check in now": "立即签到",
     "Check resolved IPs against IP filters even when accessing by domain": "即使通过域名访问,也对照 IP 过滤器检查解析的 IP",
     "Check resolved IPs against IP filters even when accessing by domain": "即使通过域名访问,也对照 IP 过滤器检查解析的 IP",
     "Check-in failed": "签到失败",
     "Check-in failed": "签到失败",
+    "Check-in Rewards": "签到奖励",
     "Check-in Settings": "签到设置",
     "Check-in Settings": "签到设置",
     "Check-in successful! Received": "签到成功!获得",
     "Check-in successful! Received": "签到成功!获得",
     "Checked in": "已签到",
     "Checked in": "已签到",
@@ -783,14 +785,18 @@
     "Configure basic system information and branding": "配置基本系统信息和品牌",
     "Configure basic system information and branding": "配置基本系统信息和品牌",
     "Configure channel affinity (sticky routing) rules": "配置渠道亲和性(粘滞选路)规则",
     "Configure channel affinity (sticky routing) rules": "配置渠道亲和性(粘滞选路)规则",
     "Configure Creem products. Provide a JSON array.": "配置 Creem 产品。提供 JSON 数组。",
     "Configure Creem products. Provide a JSON array.": "配置 Creem 产品。提供 JSON 数组。",
+    "Configure currency conversion and quota display options": "配置货币换算和额度展示选项",
     "Configure custom OAuth providers for user authentication": "配置自定义OAuth提供商用于用户认证",
     "Configure custom OAuth providers for user authentication": "配置自定义OAuth提供商用于用户认证",
     "Configure daily check-in rewards for users": "配置用户每日签到奖励",
     "Configure daily check-in rewards for users": "配置用户每日签到奖励",
     "Configure discount rates based on recharge amounts": "配置基于充值金额的折扣率",
     "Configure discount rates based on recharge amounts": "配置基于充值金额的折扣率",
     "Configure experimental data export for the dashboard": "配置仪表板的实验性数据导出",
     "Configure experimental data export for the dashboard": "配置仪表板的实验性数据导出",
     "Configure Gemini safety behavior, version overrides, and thinking adapter": "配置 Gemini 安全行为、版本覆盖和思维适配器",
     "Configure Gemini safety behavior, version overrides, and thinking adapter": "配置 Gemini 安全行为、版本覆盖和思维适配器",
+    "Configure group ratios and group-specific pricing rules": "配置分组倍率和分组专属定价规则",
     "Configure in your Creem dashboard": "在您的 Creem 仪表板中配置",
     "Configure in your Creem dashboard": "在您的 Creem 仪表板中配置",
     "Configure io.net API key for model deployments": "配置 io.net API Key 用于模型部署",
     "Configure io.net API key for model deployments": "配置 io.net API Key 用于模型部署",
     "Configure keyword filtering for prompts and responses.": "配置用于提示和响应的关键词过滤。",
     "Configure keyword filtering for prompts and responses.": "配置用于提示和响应的关键词过滤。",
+    "Configure model deployment provider settings": "配置模型部署提供商设置",
+    "Configure model pricing ratios and tool prices": "配置模型定价倍率和工具价格",
     "Configure model, caching, and group ratios used for billing": "配置用于计费的模型、缓存和分组比例",
     "Configure model, caching, and group ratios used for billing": "配置用于计费的模型、缓存和分组比例",
     "Configure monitoring status page groups for the dashboard": "配置用于仪表板的监控状态页面分组",
     "Configure monitoring status page groups for the dashboard": "配置用于仪表板的监控状态页面分组",
     "Configure outgoing email server for notifications": "配置用于通知的发送邮件服务器",
     "Configure outgoing email server for notifications": "配置用于通知的发送邮件服务器",
@@ -849,6 +855,7 @@
     "Console": "控制台",
     "Console": "控制台",
     "Console area": "控制台区域",
     "Console area": "控制台区域",
     "Console Area": "控制台区域",
     "Console Area": "控制台区域",
+    "Console Content": "控制台内容",
     "Consume": "消耗",
     "Consume": "消耗",
     "Container": "容器",
     "Container": "容器",
     "Container name": "容器名称",
     "Container name": "容器名称",
@@ -970,6 +977,7 @@
     "Cross-group retry": "跨分组重试",
     "Cross-group retry": "跨分组重试",
     "Curate quick links to your different Domains": "整理到不同域的快速链接",
     "Curate quick links to your different Domains": "整理到不同域的快速链接",
     "Currency": "货币",
     "Currency": "货币",
+    "Currency & Display": "货币与展示",
     "Current Balance": "当前余额",
     "Current Balance": "当前余额",
     "Current Billing": "当前计费",
     "Current Billing": "当前计费",
     "Current Cache Size": "当前缓存大小",
     "Current Cache Size": "当前缓存大小",
@@ -1027,6 +1035,7 @@
     "Date and time when this announcement should be displayed": "此公告应显示的日期和时间",
     "Date and time when this announcement should be displayed": "此公告应显示的日期和时间",
     "Date Range": "时间范围",
     "Date Range": "时间范围",
     "Day": "天",
     "Day": "天",
+    "Day of month": "日期(日)",
     "days": "天",
     "days": "天",
     "Days to Retain": "保留天数",
     "Days to Retain": "保留天数",
     "Deducted by subscription": "由订阅抵扣",
     "Deducted by subscription": "由订阅抵扣",
@@ -1778,6 +1787,8 @@
     "GPU count": "GPU 数量",
     "GPU count": "GPU 数量",
     "Greater Than": "大于",
     "Greater Than": "大于",
     "Greater Than or Equal": "大于等于",
     "Greater Than or Equal": "大于等于",
+    "Greater than": "大于",
+    "Greater than or equal": "大于等于",
     "Grok": "Grok",
     "Grok": "Grok",
     "Grok Settings": "Grok 设置",
     "Grok Settings": "Grok 设置",
     "Group": "分组",
     "Group": "分组",
@@ -1793,6 +1804,7 @@
     "Group Name": "分组名称",
     "Group Name": "分组名称",
     "Group name cannot be changed when editing.": "编辑时无法更改组名称。",
     "Group name cannot be changed when editing.": "编辑时无法更改组名称。",
     "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.": "该表达式不是标准分档计费表达式,无法展开分组价格。",
     "Group prices cannot be expanded because this expression is not a standard tiered pricing expression.": "该表达式不是标准分档计费表达式,无法展开分组价格。",
+    "Group Pricing": "分组定价",
     "group ratio": "分组倍率",
     "group ratio": "分组倍率",
     "Group Ratio": "分组倍率",
     "Group Ratio": "分组倍率",
     "Group ratios": "分组比例",
     "Group ratios": "分组比例",
@@ -1848,6 +1860,7 @@
     "Home Page Content": "首页内容",
     "Home Page Content": "首页内容",
     "Hostname or IP of your SMTP provider": "您的 SMTP 提供商的主机名或 IP",
     "Hostname or IP of your SMTP provider": "您的 SMTP 提供商的主机名或 IP",
     "Hour": "小时",
     "Hour": "小时",
+    "Hour of day": "小时",
     "Hourly token usage by model across the last 24 hours": "过去 24 小时内各模型的逐小时 Token 用量",
     "Hourly token usage by model across the last 24 hours": "过去 24 小时内各模型的逐小时 Token 用量",
     "Hourly token usage by model over the past 24 hours": "过去 24 小时内按模型分布的小时级 Token 使用量",
     "Hourly token usage by model over the past 24 hours": "过去 24 小时内按模型分布的小时级 Token 使用量",
     "hours": "小时",
     "hours": "小时",
@@ -1893,6 +1906,7 @@
     "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "如果上游错误包含以下任何关键字(不区分大小写),渠道将自动禁用。",
     "If an upstream error contains any of these keywords (case insensitive), the channel will be disabled automatically.": "如果上游错误包含以下任何关键字(不区分大小写),渠道将自动禁用。",
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "授权成功后,生成的 JSON 将插入密钥字段。您仍需保存频道以持久化。",
     "If authorization succeeds, the generated JSON will be inserted into the key field. You still need to save the channel to persist it.": "授权成功后,生成的 JSON 将插入密钥字段。您仍需保存频道以持久化。",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "如果连接上游 One API 或 New API 中继项目,除非您知道自己在做什么,否则请使用 OpenAI 类型",
     "If connecting to upstream One API or New API relay projects, use OpenAI type instead unless you know what you are doing": "如果连接上游 One API 或 New API 中继项目,除非您知道自己在做什么,否则请使用 OpenAI 类型",
+    "If this keeps happening, please report it on GitHub Issues.": "如果问题持续出现,请到 GitHub Issues 反馈。",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。",
     "If the affinity channel fails and retry succeeds on another channel, update affinity to the successful channel.": "如果亲和到的渠道失败,重试到其他渠道成功后,将亲和更新到成功的渠道。",
     "Ignored upstream models": "已忽略上游模型",
     "Ignored upstream models": "已忽略上游模型",
     "Image": "图片",
     "Image": "图片",
@@ -2056,6 +2070,8 @@
     "Less": "更少",
     "Less": "更少",
     "Less Than": "小于",
     "Less Than": "小于",
     "Less Than or Equal": "小于等于",
     "Less Than or Equal": "小于等于",
+    "Less than": "小于",
+    "Less than or equal": "小于等于",
     "License": "许可证",
     "License": "许可证",
     "Light": "浅色",
     "Light": "浅色",
     "Lightning Fast": "极速",
     "Lightning Fast": "极速",
@@ -2217,6 +2233,7 @@
     "Model context usage": "模型上下文用量",
     "Model context usage": "模型上下文用量",
     "Model deleted": "模型已删除",
     "Model deleted": "模型已删除",
     "Model deleted successfully": "模型删除成功",
     "Model deleted successfully": "模型删除成功",
+    "Model Deployment": "模型部署",
     "Model deployment service is disabled": "模型部署服务未启用",
     "Model deployment service is disabled": "模型部署服务未启用",
     "Model Description": "模型描述",
     "Model Description": "模型描述",
     "Model details": "模型详情",
     "Model details": "模型详情",
@@ -2256,6 +2273,7 @@
     "Models": "模型",
     "Models": "模型",
     "Models *": "模型 *",
     "Models *": "模型 *",
     "Models & Groups": "模型与分组",
     "Models & Groups": "模型与分组",
+    "Models & Routing": "模型与路由",
     "Models appended successfully": "模型已追加成功",
     "Models appended successfully": "模型已追加成功",
     "Models are required": "需要模型",
     "Models are required": "需要模型",
     "Models climbing the leaderboard": "正在攀升的模型",
     "Models climbing the leaderboard": "正在攀升的模型",
@@ -2276,6 +2294,7 @@
     "Monitor": "监控",
     "Monitor": "监控",
     "Monitoring & Alerts": "监控与警报",
     "Monitoring & Alerts": "监控与警报",
     "Month": "本月",
     "Month": "本月",
+    "Month number": "月份",
     "Monthly": "每月",
     "Monthly": "每月",
     "Monthly tokens": "每月 token",
     "Monthly tokens": "每月 token",
     "months": "个月",
     "months": "个月",
@@ -2592,6 +2611,7 @@
     "Operation": "操作",
     "Operation": "操作",
     "Operation failed": "操作失败",
     "Operation failed": "操作失败",
     "Operation Type": "操作类型",
     "Operation Type": "操作类型",
+    "Operations": "运维",
     "Operator Admin": "操作管理员",
     "Operator Admin": "操作管理员",
     "Optimize system for self-hosted single-user usage": "优化系统以适应自托管单用户使用",
     "Optimize system for self-hosted single-user usage": "优化系统以适应自托管单用户使用",
     "Optimized network architecture ensures millisecond response times": "优化的网络架构,确保毫秒级响应时间",
     "Optimized network architecture ensures millisecond response times": "优化的网络架构,确保毫秒级响应时间",
@@ -2636,6 +2656,7 @@
     "Override Rules": "覆盖规则",
     "Override Rules": "覆盖规则",
     "Override the endpoint used for testing. Leave empty to auto detect.": "覆盖用于测试的端点。留空以自动检测。",
     "Override the endpoint used for testing. Leave empty to auto detect.": "覆盖用于测试的端点。留空以自动检测。",
     "overrides for matching model prefix.": "为匹配模型前缀的覆盖价。",
     "overrides for matching model prefix.": "为匹配模型前缀的覆盖价。",
+    "Overnight range": "跨日范围",
     "Overview": "概览",
     "Overview": "概览",
     "Overwritten": "已覆盖",
     "Overwritten": "已覆盖",
     "Page": "页面",
     "Page": "页面",
@@ -3102,6 +3123,7 @@
     "Replacement Model": "替换模型",
     "Replacement Model": "替换模型",
     "Replica count": "副本数",
     "Replica count": "副本数",
     "Replicate": "Replicate",
     "Replicate": "Replicate",
+    "Report an issue": "反馈问题",
     "request": "请求",
     "request": "请求",
     "Request": "请求",
     "Request": "请求",
     "Request Body Disk Cache": "请求体磁盘缓存",
     "Request Body Disk Cache": "请求体磁盘缓存",
@@ -3308,6 +3330,7 @@
     "Secret Key": "密钥",
     "Secret Key": "密钥",
     "Secure & Reliable": "安全可靠",
     "Secure & Reliable": "安全可靠",
     "Security": "安全",
     "Security": "安全",
+    "Security & Limits": "安全与限制",
     "Security Check": "安全验证",
     "Security Check": "安全验证",
     "Security verification": "安全验证",
     "Security verification": "安全验证",
     "Select": "选择",
     "Select": "选择",
@@ -3456,6 +3479,7 @@
     "Simple mode only returns message; status code and error type use system defaults.": "简洁模式仅返回 message;状态码和错误类型将使用系统默认值。",
     "Simple mode only returns message; status code and error type use system defaults.": "简洁模式仅返回 message;状态码和错误类型将使用系统默认值。",
     "Simple mode: prune objects by type, e.g. redacted_thinking.": "简洁模式:按 type 全量清理对象,例如 redacted_thinking。",
     "Simple mode: prune objects by type, e.g. redacted_thinking.": "简洁模式:按 type 全量清理对象,例如 redacted_thinking。",
     "Single Key": "单密钥",
     "Single Key": "单密钥",
+    "Site & Branding": "站点与品牌",
     "Site Key": "站点密钥",
     "Site Key": "站点密钥",
     "Size:": "大小:",
     "Size:": "大小:",
     "sk_xxx or rk_xxx": "sk_xxx 或 rk_xxx",
     "sk_xxx or rk_xxx": "sk_xxx 或 rk_xxx",
@@ -3733,6 +3757,8 @@
     "Tier": "档位",
     "Tier": "档位",
     "Tier name": "档位名称",
     "Tier name": "档位名称",
     "Tiered": "阶梯",
     "Tiered": "阶梯",
+    "Token prices": "Token 价格",
+    "Each tier supports up to 2 conditions. The last tier without conditions is the fallback.": "每个档位最多 2 个条件,最后一个无条件档位为兜底档。",
     "Tiered (billing expression)": "阶梯计费(计费表达式)",
     "Tiered (billing expression)": "阶梯计费(计费表达式)",
     "Tiered price table": "分档价格表",
     "Tiered price table": "分档价格表",
     "Time": "时间",
     "Time": "时间",
@@ -3741,7 +3767,7 @@
     "Time window for rate limiting": "速率限制的时间窗口",
     "Time window for rate limiting": "速率限制的时间窗口",
     "Time-based": "含时间条件",
     "Time-based": "含时间条件",
     "Time:": "时间:",
     "Time:": "时间:",
-    "Timed cache (1h)": "定时缓存(1 小时)",
+    "Time-sliced cache (Claude)": "分时缓存(Claude)",
     "Timeline": "时间线",
     "Timeline": "时间线",
     "times": "次",
     "times": "次",
     "Timing": "耗时",
     "Timing": "耗时",
@@ -4219,6 +4245,71 @@
     "Zero retention": "零数据保留",
     "Zero retention": "零数据保留",
     "Zhipu": "智谱",
     "Zhipu": "智谱",
     "Zhipu V4": "智谱 V4",
     "Zhipu V4": "智谱 V4",
-    "Zoom": "缩放"
+    "Zoom": "缩放",
+    "Add model pricing": "添加模型定价",
+    "Applied {{name}} pricing to {{count}} models": "已将 {{name}} 的定价应用到 {{count}} 个模型",
+    "Audio input price": "音频输入价格",
+    "Audio output price": "音频输出价格",
+    "Audio output price requires an audio input price.": "音频输出价格需要先填写音频输入价格。",
+    "Billable input tokens": "计费输入 token",
+    "Billable output tokens": "计费输出 token",
+    "Cache create (1h) price": "1 小时缓存写入价格",
+    "Cache create price": "缓存写入价格",
+    "Cache read price": "缓存读取价格",
+    "Cache pricing": "缓存价格",
+    "Cache write price": "缓存写入价格",
+    "Changes are written to the settings draft on save.": "保存后会写入设置草稿。",
+    "Clean": "无冲突",
+    "Completion price": "补全价格",
+    "Core pricing": "核心价格",
+    "Copy {{name}} pricing": "复制 {{name}} 定价",
+    "Disabled lanes are omitted on save.": "关闭的价格通道保存时会被省略。",
+    "Edit model pricing": "编辑模型定价",
+    "Empty": "空",
+    "Each tier supports up to 2 conditions; the last tier is the catch-all without conditions. Use full input length for tier conditions to avoid mis-routing when cache hits reduce billable input tokens.": "每个档位最多支持 2 个条件;最后一个档位是不带条件的兜底档。建议使用完整输入长度作为档位条件,避免缓存命中减少计费输入 token 后误判档位。",
+    "All conditions must match before this tier is used.": "所有条件都满足后才会使用该档位。",
+    "Base input and output token prices for this tier.": "该档位的基础输入与输出 token 价格。",
+    "Expression": "表达式",
+    "Fallback tier": "兜底档位",
+    "Full input length": "完整输入长度",
+    "Input price is required before saving dependent prices.": "保存依赖价格前必须先填写输入价格。",
+    "Image input price": "图像输入价格",
+    "Image output price": "图像输出价格",
+    "Media pricing": "多模态价格",
+    "New model": "新模型",
+    "No separate media pricing configured.": "未单独配置多模态价格。",
+    "Open a source model first": "请先打开一个源模型",
+    "Output token price for generated tokens.": "生成内容的输出 token 价格。",
+    "Per-request": "按次",
+    "Save preview": "保存预览",
+    "Select at least one target model": "请至少选择一个目标模型",
+    "Separate image/audio prices are enabled.": "已启用独立图像/音频价格。",
+    "Set separate prices for cache reads and writes.": "为缓存读取与写入设置独立价格。",
+    "This model has both fixed-price and ratio settings. Saving the current mode will rewrite the conflicting fields.": "该模型同时存在固定价格和比例设置。保存当前模式会重写冲突字段。",
+    "This model has both fixed-price and token-price settings. Saving the current mode will rewrite the conflicting fields.": "该模型同时存在固定价格和按 token 价格设置。保存当前模式会重写冲突字段。",
+    "This tier catches any request that did not match earlier tiers.": "该档位会兜底匹配所有未命中前面档位的请求。",
+    "Tier conditions": "档位条件",
+    "Token price for audio input.": "音频输入 token 价格。",
+    "Token price for audio output.": "音频输出 token 价格。",
+    "Token price for cache reads.": "缓存读取 token 价格。",
+    "Token price for creating cache entries.": "创建缓存条目的 token 价格。",
+    "Token price for image input.": "图像输入 token 价格。",
+    "USD price per 1M input tokens.": "每 100 万输入 token 的美元价格。",
+    "USD price per 1M tokens.": "每 100 万 token 的美元价格。",
+    "{{count}} selected targets available for bulk copy.": "已选择 {{count}} 个目标,可用于批量复制。",
+    "Base input price only": "仅基础输入价格",
+    "Expression based": "基于表达式",
+    "Expression pricing": "表达式计费",
+    "Fixed request price": "固定按次价格",
+    "Includes request rules": "包含请求规则",
+    "No base input price": "未设置基础输入价格",
+    "No models configured. Use Add model to get started.": "暂无模型配置。使用添加模型开始配置。",
+    "Price summary": "价格摘要",
+    "Select a model to edit pricing": "选择一个模型编辑定价",
+    "Tiered pricing": "阶梯计费",
+    "Unset price": "未设置价格",
+    "Use the full-width table to scan prices, then select a row to edit it here.": "先在表格中快速浏览价格,然后选择一行在这里编辑。",
+    "extras": "额外项",
+    "tiers": "档"
   }
   }
 }
 }

+ 176 - 176
web/default/src/routeTree.gen.ts

@@ -51,19 +51,19 @@ import { Route as AuthenticatedErrorsErrorRouteImport } from './routes/_authenti
 import { Route as AuthenticatedDashboardSectionRouteImport } from './routes/_authenticated/dashboard/$section'
 import { Route as AuthenticatedDashboardSectionRouteImport } from './routes/_authenticated/dashboard/$section'
 import { Route as AuthenticatedChatChatIdRouteImport } from './routes/_authenticated/chat/$chatId'
 import { Route as AuthenticatedChatChatIdRouteImport } from './routes/_authenticated/chat/$chatId'
 import { Route as authUserResetRouteImport } from './routes/(auth)/user/reset'
 import { Route as authUserResetRouteImport } from './routes/(auth)/user/reset'
-import { Route as AuthenticatedSystemSettingsRequestLimitsIndexRouteImport } from './routes/_authenticated/system-settings/request-limits/index'
+import { Route as AuthenticatedSystemSettingsSiteIndexRouteImport } from './routes/_authenticated/system-settings/site/index'
+import { Route as AuthenticatedSystemSettingsSecurityIndexRouteImport } from './routes/_authenticated/system-settings/security/index'
+import { Route as AuthenticatedSystemSettingsOperationsIndexRouteImport } from './routes/_authenticated/system-settings/operations/index'
 import { Route as AuthenticatedSystemSettingsModelsIndexRouteImport } from './routes/_authenticated/system-settings/models/index'
 import { Route as AuthenticatedSystemSettingsModelsIndexRouteImport } from './routes/_authenticated/system-settings/models/index'
-import { Route as AuthenticatedSystemSettingsMaintenanceIndexRouteImport } from './routes/_authenticated/system-settings/maintenance/index'
-import { Route as AuthenticatedSystemSettingsIntegrationsIndexRouteImport } from './routes/_authenticated/system-settings/integrations/index'
-import { Route as AuthenticatedSystemSettingsGeneralIndexRouteImport } from './routes/_authenticated/system-settings/general/index'
 import { Route as AuthenticatedSystemSettingsContentIndexRouteImport } from './routes/_authenticated/system-settings/content/index'
 import { Route as AuthenticatedSystemSettingsContentIndexRouteImport } from './routes/_authenticated/system-settings/content/index'
+import { Route as AuthenticatedSystemSettingsBillingIndexRouteImport } from './routes/_authenticated/system-settings/billing/index'
 import { Route as AuthenticatedSystemSettingsAuthIndexRouteImport } from './routes/_authenticated/system-settings/auth/index'
 import { Route as AuthenticatedSystemSettingsAuthIndexRouteImport } from './routes/_authenticated/system-settings/auth/index'
-import { Route as AuthenticatedSystemSettingsRequestLimitsSectionRouteImport } from './routes/_authenticated/system-settings/request-limits/$section'
+import { Route as AuthenticatedSystemSettingsSiteSectionRouteImport } from './routes/_authenticated/system-settings/site/$section'
+import { Route as AuthenticatedSystemSettingsSecuritySectionRouteImport } from './routes/_authenticated/system-settings/security/$section'
+import { Route as AuthenticatedSystemSettingsOperationsSectionRouteImport } from './routes/_authenticated/system-settings/operations/$section'
 import { Route as AuthenticatedSystemSettingsModelsSectionRouteImport } from './routes/_authenticated/system-settings/models/$section'
 import { Route as AuthenticatedSystemSettingsModelsSectionRouteImport } from './routes/_authenticated/system-settings/models/$section'
-import { Route as AuthenticatedSystemSettingsMaintenanceSectionRouteImport } from './routes/_authenticated/system-settings/maintenance/$section'
-import { Route as AuthenticatedSystemSettingsIntegrationsSectionRouteImport } from './routes/_authenticated/system-settings/integrations/$section'
-import { Route as AuthenticatedSystemSettingsGeneralSectionRouteImport } from './routes/_authenticated/system-settings/general/$section'
 import { Route as AuthenticatedSystemSettingsContentSectionRouteImport } from './routes/_authenticated/system-settings/content/$section'
 import { Route as AuthenticatedSystemSettingsContentSectionRouteImport } from './routes/_authenticated/system-settings/content/$section'
+import { Route as AuthenticatedSystemSettingsBillingSectionRouteImport } from './routes/_authenticated/system-settings/billing/$section'
 import { Route as AuthenticatedSystemSettingsAuthSectionRouteImport } from './routes/_authenticated/system-settings/auth/$section'
 import { Route as AuthenticatedSystemSettingsAuthSectionRouteImport } from './routes/_authenticated/system-settings/auth/$section'
 
 
 const UserAgreementRoute = UserAgreementRouteImport.update({
 const UserAgreementRoute = UserAgreementRouteImport.update({
@@ -289,34 +289,28 @@ const authUserResetRoute = authUserResetRouteImport.update({
   path: '/user/reset',
   path: '/user/reset',
   getParentRoute: () => authRouteRoute,
   getParentRoute: () => authRouteRoute,
 } as any)
 } as any)
-const AuthenticatedSystemSettingsRequestLimitsIndexRoute =
-  AuthenticatedSystemSettingsRequestLimitsIndexRouteImport.update({
-    id: '/request-limits/',
-    path: '/request-limits/',
+const AuthenticatedSystemSettingsSiteIndexRoute =
+  AuthenticatedSystemSettingsSiteIndexRouteImport.update({
+    id: '/site/',
+    path: '/site/',
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
   } as any)
   } as any)
-const AuthenticatedSystemSettingsModelsIndexRoute =
-  AuthenticatedSystemSettingsModelsIndexRouteImport.update({
-    id: '/models/',
-    path: '/models/',
-    getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
-  } as any)
-const AuthenticatedSystemSettingsMaintenanceIndexRoute =
-  AuthenticatedSystemSettingsMaintenanceIndexRouteImport.update({
-    id: '/maintenance/',
-    path: '/maintenance/',
+const AuthenticatedSystemSettingsSecurityIndexRoute =
+  AuthenticatedSystemSettingsSecurityIndexRouteImport.update({
+    id: '/security/',
+    path: '/security/',
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
   } as any)
   } as any)
-const AuthenticatedSystemSettingsIntegrationsIndexRoute =
-  AuthenticatedSystemSettingsIntegrationsIndexRouteImport.update({
-    id: '/integrations/',
-    path: '/integrations/',
+const AuthenticatedSystemSettingsOperationsIndexRoute =
+  AuthenticatedSystemSettingsOperationsIndexRouteImport.update({
+    id: '/operations/',
+    path: '/operations/',
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
   } as any)
   } as any)
-const AuthenticatedSystemSettingsGeneralIndexRoute =
-  AuthenticatedSystemSettingsGeneralIndexRouteImport.update({
-    id: '/general/',
-    path: '/general/',
+const AuthenticatedSystemSettingsModelsIndexRoute =
+  AuthenticatedSystemSettingsModelsIndexRouteImport.update({
+    id: '/models/',
+    path: '/models/',
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
   } as any)
   } as any)
 const AuthenticatedSystemSettingsContentIndexRoute =
 const AuthenticatedSystemSettingsContentIndexRoute =
@@ -325,40 +319,40 @@ const AuthenticatedSystemSettingsContentIndexRoute =
     path: '/content/',
     path: '/content/',
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
   } as any)
   } as any)
+const AuthenticatedSystemSettingsBillingIndexRoute =
+  AuthenticatedSystemSettingsBillingIndexRouteImport.update({
+    id: '/billing/',
+    path: '/billing/',
+    getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
+  } as any)
 const AuthenticatedSystemSettingsAuthIndexRoute =
 const AuthenticatedSystemSettingsAuthIndexRoute =
   AuthenticatedSystemSettingsAuthIndexRouteImport.update({
   AuthenticatedSystemSettingsAuthIndexRouteImport.update({
     id: '/auth/',
     id: '/auth/',
     path: '/auth/',
     path: '/auth/',
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
   } as any)
   } as any)
-const AuthenticatedSystemSettingsRequestLimitsSectionRoute =
-  AuthenticatedSystemSettingsRequestLimitsSectionRouteImport.update({
-    id: '/request-limits/$section',
-    path: '/request-limits/$section',
+const AuthenticatedSystemSettingsSiteSectionRoute =
+  AuthenticatedSystemSettingsSiteSectionRouteImport.update({
+    id: '/site/$section',
+    path: '/site/$section',
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
   } as any)
   } as any)
-const AuthenticatedSystemSettingsModelsSectionRoute =
-  AuthenticatedSystemSettingsModelsSectionRouteImport.update({
-    id: '/models/$section',
-    path: '/models/$section',
-    getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
-  } as any)
-const AuthenticatedSystemSettingsMaintenanceSectionRoute =
-  AuthenticatedSystemSettingsMaintenanceSectionRouteImport.update({
-    id: '/maintenance/$section',
-    path: '/maintenance/$section',
+const AuthenticatedSystemSettingsSecuritySectionRoute =
+  AuthenticatedSystemSettingsSecuritySectionRouteImport.update({
+    id: '/security/$section',
+    path: '/security/$section',
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
   } as any)
   } as any)
-const AuthenticatedSystemSettingsIntegrationsSectionRoute =
-  AuthenticatedSystemSettingsIntegrationsSectionRouteImport.update({
-    id: '/integrations/$section',
-    path: '/integrations/$section',
+const AuthenticatedSystemSettingsOperationsSectionRoute =
+  AuthenticatedSystemSettingsOperationsSectionRouteImport.update({
+    id: '/operations/$section',
+    path: '/operations/$section',
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
   } as any)
   } as any)
-const AuthenticatedSystemSettingsGeneralSectionRoute =
-  AuthenticatedSystemSettingsGeneralSectionRouteImport.update({
-    id: '/general/$section',
-    path: '/general/$section',
+const AuthenticatedSystemSettingsModelsSectionRoute =
+  AuthenticatedSystemSettingsModelsSectionRouteImport.update({
+    id: '/models/$section',
+    path: '/models/$section',
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
   } as any)
   } as any)
 const AuthenticatedSystemSettingsContentSectionRoute =
 const AuthenticatedSystemSettingsContentSectionRoute =
@@ -367,6 +361,12 @@ const AuthenticatedSystemSettingsContentSectionRoute =
     path: '/content/$section',
     path: '/content/$section',
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
     getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
   } as any)
   } as any)
+const AuthenticatedSystemSettingsBillingSectionRoute =
+  AuthenticatedSystemSettingsBillingSectionRouteImport.update({
+    id: '/billing/$section',
+    path: '/billing/$section',
+    getParentRoute: () => AuthenticatedSystemSettingsRouteRoute,
+  } as any)
 const AuthenticatedSystemSettingsAuthSectionRoute =
 const AuthenticatedSystemSettingsAuthSectionRoute =
   AuthenticatedSystemSettingsAuthSectionRouteImport.update({
   AuthenticatedSystemSettingsAuthSectionRouteImport.update({
     id: '/auth/$section',
     id: '/auth/$section',
@@ -416,19 +416,19 @@ export interface FileRoutesByFullPath {
   '/wallet/': typeof AuthenticatedWalletIndexRoute
   '/wallet/': typeof AuthenticatedWalletIndexRoute
   '/pricing/$modelId/': typeof PricingModelIdIndexRoute
   '/pricing/$modelId/': typeof PricingModelIdIndexRoute
   '/system-settings/auth/$section': typeof AuthenticatedSystemSettingsAuthSectionRoute
   '/system-settings/auth/$section': typeof AuthenticatedSystemSettingsAuthSectionRoute
+  '/system-settings/billing/$section': typeof AuthenticatedSystemSettingsBillingSectionRoute
   '/system-settings/content/$section': typeof AuthenticatedSystemSettingsContentSectionRoute
   '/system-settings/content/$section': typeof AuthenticatedSystemSettingsContentSectionRoute
-  '/system-settings/general/$section': typeof AuthenticatedSystemSettingsGeneralSectionRoute
-  '/system-settings/integrations/$section': typeof AuthenticatedSystemSettingsIntegrationsSectionRoute
-  '/system-settings/maintenance/$section': typeof AuthenticatedSystemSettingsMaintenanceSectionRoute
   '/system-settings/models/$section': typeof AuthenticatedSystemSettingsModelsSectionRoute
   '/system-settings/models/$section': typeof AuthenticatedSystemSettingsModelsSectionRoute
-  '/system-settings/request-limits/$section': typeof AuthenticatedSystemSettingsRequestLimitsSectionRoute
+  '/system-settings/operations/$section': typeof AuthenticatedSystemSettingsOperationsSectionRoute
+  '/system-settings/security/$section': typeof AuthenticatedSystemSettingsSecuritySectionRoute
+  '/system-settings/site/$section': typeof AuthenticatedSystemSettingsSiteSectionRoute
   '/system-settings/auth/': typeof AuthenticatedSystemSettingsAuthIndexRoute
   '/system-settings/auth/': typeof AuthenticatedSystemSettingsAuthIndexRoute
+  '/system-settings/billing/': typeof AuthenticatedSystemSettingsBillingIndexRoute
   '/system-settings/content/': typeof AuthenticatedSystemSettingsContentIndexRoute
   '/system-settings/content/': typeof AuthenticatedSystemSettingsContentIndexRoute
-  '/system-settings/general/': typeof AuthenticatedSystemSettingsGeneralIndexRoute
-  '/system-settings/integrations/': typeof AuthenticatedSystemSettingsIntegrationsIndexRoute
-  '/system-settings/maintenance/': typeof AuthenticatedSystemSettingsMaintenanceIndexRoute
   '/system-settings/models/': typeof AuthenticatedSystemSettingsModelsIndexRoute
   '/system-settings/models/': typeof AuthenticatedSystemSettingsModelsIndexRoute
-  '/system-settings/request-limits/': typeof AuthenticatedSystemSettingsRequestLimitsIndexRoute
+  '/system-settings/operations/': typeof AuthenticatedSystemSettingsOperationsIndexRoute
+  '/system-settings/security/': typeof AuthenticatedSystemSettingsSecurityIndexRoute
+  '/system-settings/site/': typeof AuthenticatedSystemSettingsSiteIndexRoute
 }
 }
 export interface FileRoutesByTo {
 export interface FileRoutesByTo {
   '/': typeof IndexRoute
   '/': typeof IndexRoute
@@ -471,19 +471,19 @@ export interface FileRoutesByTo {
   '/wallet': typeof AuthenticatedWalletIndexRoute
   '/wallet': typeof AuthenticatedWalletIndexRoute
   '/pricing/$modelId': typeof PricingModelIdIndexRoute
   '/pricing/$modelId': typeof PricingModelIdIndexRoute
   '/system-settings/auth/$section': typeof AuthenticatedSystemSettingsAuthSectionRoute
   '/system-settings/auth/$section': typeof AuthenticatedSystemSettingsAuthSectionRoute
+  '/system-settings/billing/$section': typeof AuthenticatedSystemSettingsBillingSectionRoute
   '/system-settings/content/$section': typeof AuthenticatedSystemSettingsContentSectionRoute
   '/system-settings/content/$section': typeof AuthenticatedSystemSettingsContentSectionRoute
-  '/system-settings/general/$section': typeof AuthenticatedSystemSettingsGeneralSectionRoute
-  '/system-settings/integrations/$section': typeof AuthenticatedSystemSettingsIntegrationsSectionRoute
-  '/system-settings/maintenance/$section': typeof AuthenticatedSystemSettingsMaintenanceSectionRoute
   '/system-settings/models/$section': typeof AuthenticatedSystemSettingsModelsSectionRoute
   '/system-settings/models/$section': typeof AuthenticatedSystemSettingsModelsSectionRoute
-  '/system-settings/request-limits/$section': typeof AuthenticatedSystemSettingsRequestLimitsSectionRoute
+  '/system-settings/operations/$section': typeof AuthenticatedSystemSettingsOperationsSectionRoute
+  '/system-settings/security/$section': typeof AuthenticatedSystemSettingsSecuritySectionRoute
+  '/system-settings/site/$section': typeof AuthenticatedSystemSettingsSiteSectionRoute
   '/system-settings/auth': typeof AuthenticatedSystemSettingsAuthIndexRoute
   '/system-settings/auth': typeof AuthenticatedSystemSettingsAuthIndexRoute
+  '/system-settings/billing': typeof AuthenticatedSystemSettingsBillingIndexRoute
   '/system-settings/content': typeof AuthenticatedSystemSettingsContentIndexRoute
   '/system-settings/content': typeof AuthenticatedSystemSettingsContentIndexRoute
-  '/system-settings/general': typeof AuthenticatedSystemSettingsGeneralIndexRoute
-  '/system-settings/integrations': typeof AuthenticatedSystemSettingsIntegrationsIndexRoute
-  '/system-settings/maintenance': typeof AuthenticatedSystemSettingsMaintenanceIndexRoute
   '/system-settings/models': typeof AuthenticatedSystemSettingsModelsIndexRoute
   '/system-settings/models': typeof AuthenticatedSystemSettingsModelsIndexRoute
-  '/system-settings/request-limits': typeof AuthenticatedSystemSettingsRequestLimitsIndexRoute
+  '/system-settings/operations': typeof AuthenticatedSystemSettingsOperationsIndexRoute
+  '/system-settings/security': typeof AuthenticatedSystemSettingsSecurityIndexRoute
+  '/system-settings/site': typeof AuthenticatedSystemSettingsSiteIndexRoute
 }
 }
 export interface FileRoutesById {
 export interface FileRoutesById {
   __root__: typeof rootRouteImport
   __root__: typeof rootRouteImport
@@ -530,19 +530,19 @@ export interface FileRoutesById {
   '/_authenticated/wallet/': typeof AuthenticatedWalletIndexRoute
   '/_authenticated/wallet/': typeof AuthenticatedWalletIndexRoute
   '/pricing/$modelId/': typeof PricingModelIdIndexRoute
   '/pricing/$modelId/': typeof PricingModelIdIndexRoute
   '/_authenticated/system-settings/auth/$section': typeof AuthenticatedSystemSettingsAuthSectionRoute
   '/_authenticated/system-settings/auth/$section': typeof AuthenticatedSystemSettingsAuthSectionRoute
+  '/_authenticated/system-settings/billing/$section': typeof AuthenticatedSystemSettingsBillingSectionRoute
   '/_authenticated/system-settings/content/$section': typeof AuthenticatedSystemSettingsContentSectionRoute
   '/_authenticated/system-settings/content/$section': typeof AuthenticatedSystemSettingsContentSectionRoute
-  '/_authenticated/system-settings/general/$section': typeof AuthenticatedSystemSettingsGeneralSectionRoute
-  '/_authenticated/system-settings/integrations/$section': typeof AuthenticatedSystemSettingsIntegrationsSectionRoute
-  '/_authenticated/system-settings/maintenance/$section': typeof AuthenticatedSystemSettingsMaintenanceSectionRoute
   '/_authenticated/system-settings/models/$section': typeof AuthenticatedSystemSettingsModelsSectionRoute
   '/_authenticated/system-settings/models/$section': typeof AuthenticatedSystemSettingsModelsSectionRoute
-  '/_authenticated/system-settings/request-limits/$section': typeof AuthenticatedSystemSettingsRequestLimitsSectionRoute
+  '/_authenticated/system-settings/operations/$section': typeof AuthenticatedSystemSettingsOperationsSectionRoute
+  '/_authenticated/system-settings/security/$section': typeof AuthenticatedSystemSettingsSecuritySectionRoute
+  '/_authenticated/system-settings/site/$section': typeof AuthenticatedSystemSettingsSiteSectionRoute
   '/_authenticated/system-settings/auth/': typeof AuthenticatedSystemSettingsAuthIndexRoute
   '/_authenticated/system-settings/auth/': typeof AuthenticatedSystemSettingsAuthIndexRoute
+  '/_authenticated/system-settings/billing/': typeof AuthenticatedSystemSettingsBillingIndexRoute
   '/_authenticated/system-settings/content/': typeof AuthenticatedSystemSettingsContentIndexRoute
   '/_authenticated/system-settings/content/': typeof AuthenticatedSystemSettingsContentIndexRoute
-  '/_authenticated/system-settings/general/': typeof AuthenticatedSystemSettingsGeneralIndexRoute
-  '/_authenticated/system-settings/integrations/': typeof AuthenticatedSystemSettingsIntegrationsIndexRoute
-  '/_authenticated/system-settings/maintenance/': typeof AuthenticatedSystemSettingsMaintenanceIndexRoute
   '/_authenticated/system-settings/models/': typeof AuthenticatedSystemSettingsModelsIndexRoute
   '/_authenticated/system-settings/models/': typeof AuthenticatedSystemSettingsModelsIndexRoute
-  '/_authenticated/system-settings/request-limits/': typeof AuthenticatedSystemSettingsRequestLimitsIndexRoute
+  '/_authenticated/system-settings/operations/': typeof AuthenticatedSystemSettingsOperationsIndexRoute
+  '/_authenticated/system-settings/security/': typeof AuthenticatedSystemSettingsSecurityIndexRoute
+  '/_authenticated/system-settings/site/': typeof AuthenticatedSystemSettingsSiteIndexRoute
 }
 }
 export interface FileRouteTypes {
 export interface FileRouteTypes {
   fileRoutesByFullPath: FileRoutesByFullPath
   fileRoutesByFullPath: FileRoutesByFullPath
@@ -588,19 +588,19 @@ export interface FileRouteTypes {
     | '/wallet/'
     | '/wallet/'
     | '/pricing/$modelId/'
     | '/pricing/$modelId/'
     | '/system-settings/auth/$section'
     | '/system-settings/auth/$section'
+    | '/system-settings/billing/$section'
     | '/system-settings/content/$section'
     | '/system-settings/content/$section'
-    | '/system-settings/general/$section'
-    | '/system-settings/integrations/$section'
-    | '/system-settings/maintenance/$section'
     | '/system-settings/models/$section'
     | '/system-settings/models/$section'
-    | '/system-settings/request-limits/$section'
+    | '/system-settings/operations/$section'
+    | '/system-settings/security/$section'
+    | '/system-settings/site/$section'
     | '/system-settings/auth/'
     | '/system-settings/auth/'
+    | '/system-settings/billing/'
     | '/system-settings/content/'
     | '/system-settings/content/'
-    | '/system-settings/general/'
-    | '/system-settings/integrations/'
-    | '/system-settings/maintenance/'
     | '/system-settings/models/'
     | '/system-settings/models/'
-    | '/system-settings/request-limits/'
+    | '/system-settings/operations/'
+    | '/system-settings/security/'
+    | '/system-settings/site/'
   fileRoutesByTo: FileRoutesByTo
   fileRoutesByTo: FileRoutesByTo
   to:
   to:
     | '/'
     | '/'
@@ -643,19 +643,19 @@ export interface FileRouteTypes {
     | '/wallet'
     | '/wallet'
     | '/pricing/$modelId'
     | '/pricing/$modelId'
     | '/system-settings/auth/$section'
     | '/system-settings/auth/$section'
+    | '/system-settings/billing/$section'
     | '/system-settings/content/$section'
     | '/system-settings/content/$section'
-    | '/system-settings/general/$section'
-    | '/system-settings/integrations/$section'
-    | '/system-settings/maintenance/$section'
     | '/system-settings/models/$section'
     | '/system-settings/models/$section'
-    | '/system-settings/request-limits/$section'
+    | '/system-settings/operations/$section'
+    | '/system-settings/security/$section'
+    | '/system-settings/site/$section'
     | '/system-settings/auth'
     | '/system-settings/auth'
+    | '/system-settings/billing'
     | '/system-settings/content'
     | '/system-settings/content'
-    | '/system-settings/general'
-    | '/system-settings/integrations'
-    | '/system-settings/maintenance'
     | '/system-settings/models'
     | '/system-settings/models'
-    | '/system-settings/request-limits'
+    | '/system-settings/operations'
+    | '/system-settings/security'
+    | '/system-settings/site'
   id:
   id:
     | '__root__'
     | '__root__'
     | '/'
     | '/'
@@ -701,19 +701,19 @@ export interface FileRouteTypes {
     | '/_authenticated/wallet/'
     | '/_authenticated/wallet/'
     | '/pricing/$modelId/'
     | '/pricing/$modelId/'
     | '/_authenticated/system-settings/auth/$section'
     | '/_authenticated/system-settings/auth/$section'
+    | '/_authenticated/system-settings/billing/$section'
     | '/_authenticated/system-settings/content/$section'
     | '/_authenticated/system-settings/content/$section'
-    | '/_authenticated/system-settings/general/$section'
-    | '/_authenticated/system-settings/integrations/$section'
-    | '/_authenticated/system-settings/maintenance/$section'
     | '/_authenticated/system-settings/models/$section'
     | '/_authenticated/system-settings/models/$section'
-    | '/_authenticated/system-settings/request-limits/$section'
+    | '/_authenticated/system-settings/operations/$section'
+    | '/_authenticated/system-settings/security/$section'
+    | '/_authenticated/system-settings/site/$section'
     | '/_authenticated/system-settings/auth/'
     | '/_authenticated/system-settings/auth/'
+    | '/_authenticated/system-settings/billing/'
     | '/_authenticated/system-settings/content/'
     | '/_authenticated/system-settings/content/'
-    | '/_authenticated/system-settings/general/'
-    | '/_authenticated/system-settings/integrations/'
-    | '/_authenticated/system-settings/maintenance/'
     | '/_authenticated/system-settings/models/'
     | '/_authenticated/system-settings/models/'
-    | '/_authenticated/system-settings/request-limits/'
+    | '/_authenticated/system-settings/operations/'
+    | '/_authenticated/system-settings/security/'
+    | '/_authenticated/system-settings/site/'
   fileRoutesById: FileRoutesById
   fileRoutesById: FileRoutesById
 }
 }
 export interface RootRouteChildren {
 export interface RootRouteChildren {
@@ -1031,11 +1031,25 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof authUserResetRouteImport
       preLoaderRoute: typeof authUserResetRouteImport
       parentRoute: typeof authRouteRoute
       parentRoute: typeof authRouteRoute
     }
     }
-    '/_authenticated/system-settings/request-limits/': {
-      id: '/_authenticated/system-settings/request-limits/'
-      path: '/request-limits'
-      fullPath: '/system-settings/request-limits/'
-      preLoaderRoute: typeof AuthenticatedSystemSettingsRequestLimitsIndexRouteImport
+    '/_authenticated/system-settings/site/': {
+      id: '/_authenticated/system-settings/site/'
+      path: '/site'
+      fullPath: '/system-settings/site/'
+      preLoaderRoute: typeof AuthenticatedSystemSettingsSiteIndexRouteImport
+      parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
+    }
+    '/_authenticated/system-settings/security/': {
+      id: '/_authenticated/system-settings/security/'
+      path: '/security'
+      fullPath: '/system-settings/security/'
+      preLoaderRoute: typeof AuthenticatedSystemSettingsSecurityIndexRouteImport
+      parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
+    }
+    '/_authenticated/system-settings/operations/': {
+      id: '/_authenticated/system-settings/operations/'
+      path: '/operations'
+      fullPath: '/system-settings/operations/'
+      preLoaderRoute: typeof AuthenticatedSystemSettingsOperationsIndexRouteImport
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
     }
     }
     '/_authenticated/system-settings/models/': {
     '/_authenticated/system-settings/models/': {
@@ -1045,27 +1059,6 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof AuthenticatedSystemSettingsModelsIndexRouteImport
       preLoaderRoute: typeof AuthenticatedSystemSettingsModelsIndexRouteImport
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
     }
     }
-    '/_authenticated/system-settings/maintenance/': {
-      id: '/_authenticated/system-settings/maintenance/'
-      path: '/maintenance'
-      fullPath: '/system-settings/maintenance/'
-      preLoaderRoute: typeof AuthenticatedSystemSettingsMaintenanceIndexRouteImport
-      parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
-    }
-    '/_authenticated/system-settings/integrations/': {
-      id: '/_authenticated/system-settings/integrations/'
-      path: '/integrations'
-      fullPath: '/system-settings/integrations/'
-      preLoaderRoute: typeof AuthenticatedSystemSettingsIntegrationsIndexRouteImport
-      parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
-    }
-    '/_authenticated/system-settings/general/': {
-      id: '/_authenticated/system-settings/general/'
-      path: '/general'
-      fullPath: '/system-settings/general/'
-      preLoaderRoute: typeof AuthenticatedSystemSettingsGeneralIndexRouteImport
-      parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
-    }
     '/_authenticated/system-settings/content/': {
     '/_authenticated/system-settings/content/': {
       id: '/_authenticated/system-settings/content/'
       id: '/_authenticated/system-settings/content/'
       path: '/content'
       path: '/content'
@@ -1073,6 +1066,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof AuthenticatedSystemSettingsContentIndexRouteImport
       preLoaderRoute: typeof AuthenticatedSystemSettingsContentIndexRouteImport
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
     }
     }
+    '/_authenticated/system-settings/billing/': {
+      id: '/_authenticated/system-settings/billing/'
+      path: '/billing'
+      fullPath: '/system-settings/billing/'
+      preLoaderRoute: typeof AuthenticatedSystemSettingsBillingIndexRouteImport
+      parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
+    }
     '/_authenticated/system-settings/auth/': {
     '/_authenticated/system-settings/auth/': {
       id: '/_authenticated/system-settings/auth/'
       id: '/_authenticated/system-settings/auth/'
       path: '/auth'
       path: '/auth'
@@ -1080,11 +1080,25 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof AuthenticatedSystemSettingsAuthIndexRouteImport
       preLoaderRoute: typeof AuthenticatedSystemSettingsAuthIndexRouteImport
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
     }
     }
-    '/_authenticated/system-settings/request-limits/$section': {
-      id: '/_authenticated/system-settings/request-limits/$section'
-      path: '/request-limits/$section'
-      fullPath: '/system-settings/request-limits/$section'
-      preLoaderRoute: typeof AuthenticatedSystemSettingsRequestLimitsSectionRouteImport
+    '/_authenticated/system-settings/site/$section': {
+      id: '/_authenticated/system-settings/site/$section'
+      path: '/site/$section'
+      fullPath: '/system-settings/site/$section'
+      preLoaderRoute: typeof AuthenticatedSystemSettingsSiteSectionRouteImport
+      parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
+    }
+    '/_authenticated/system-settings/security/$section': {
+      id: '/_authenticated/system-settings/security/$section'
+      path: '/security/$section'
+      fullPath: '/system-settings/security/$section'
+      preLoaderRoute: typeof AuthenticatedSystemSettingsSecuritySectionRouteImport
+      parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
+    }
+    '/_authenticated/system-settings/operations/$section': {
+      id: '/_authenticated/system-settings/operations/$section'
+      path: '/operations/$section'
+      fullPath: '/system-settings/operations/$section'
+      preLoaderRoute: typeof AuthenticatedSystemSettingsOperationsSectionRouteImport
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
     }
     }
     '/_authenticated/system-settings/models/$section': {
     '/_authenticated/system-settings/models/$section': {
@@ -1094,27 +1108,6 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof AuthenticatedSystemSettingsModelsSectionRouteImport
       preLoaderRoute: typeof AuthenticatedSystemSettingsModelsSectionRouteImport
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
     }
     }
-    '/_authenticated/system-settings/maintenance/$section': {
-      id: '/_authenticated/system-settings/maintenance/$section'
-      path: '/maintenance/$section'
-      fullPath: '/system-settings/maintenance/$section'
-      preLoaderRoute: typeof AuthenticatedSystemSettingsMaintenanceSectionRouteImport
-      parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
-    }
-    '/_authenticated/system-settings/integrations/$section': {
-      id: '/_authenticated/system-settings/integrations/$section'
-      path: '/integrations/$section'
-      fullPath: '/system-settings/integrations/$section'
-      preLoaderRoute: typeof AuthenticatedSystemSettingsIntegrationsSectionRouteImport
-      parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
-    }
-    '/_authenticated/system-settings/general/$section': {
-      id: '/_authenticated/system-settings/general/$section'
-      path: '/general/$section'
-      fullPath: '/system-settings/general/$section'
-      preLoaderRoute: typeof AuthenticatedSystemSettingsGeneralSectionRouteImport
-      parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
-    }
     '/_authenticated/system-settings/content/$section': {
     '/_authenticated/system-settings/content/$section': {
       id: '/_authenticated/system-settings/content/$section'
       id: '/_authenticated/system-settings/content/$section'
       path: '/content/$section'
       path: '/content/$section'
@@ -1122,6 +1115,13 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof AuthenticatedSystemSettingsContentSectionRouteImport
       preLoaderRoute: typeof AuthenticatedSystemSettingsContentSectionRouteImport
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
       parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
     }
     }
+    '/_authenticated/system-settings/billing/$section': {
+      id: '/_authenticated/system-settings/billing/$section'
+      path: '/billing/$section'
+      fullPath: '/system-settings/billing/$section'
+      preLoaderRoute: typeof AuthenticatedSystemSettingsBillingSectionRouteImport
+      parentRoute: typeof AuthenticatedSystemSettingsRouteRoute
+    }
     '/_authenticated/system-settings/auth/$section': {
     '/_authenticated/system-settings/auth/$section': {
       id: '/_authenticated/system-settings/auth/$section'
       id: '/_authenticated/system-settings/auth/$section'
       path: '/auth/$section'
       path: '/auth/$section'
@@ -1159,19 +1159,19 @@ const authRouteRouteWithChildren = authRouteRoute._addFileChildren(
 interface AuthenticatedSystemSettingsRouteRouteChildren {
 interface AuthenticatedSystemSettingsRouteRouteChildren {
   AuthenticatedSystemSettingsIndexRoute: typeof AuthenticatedSystemSettingsIndexRoute
   AuthenticatedSystemSettingsIndexRoute: typeof AuthenticatedSystemSettingsIndexRoute
   AuthenticatedSystemSettingsAuthSectionRoute: typeof AuthenticatedSystemSettingsAuthSectionRoute
   AuthenticatedSystemSettingsAuthSectionRoute: typeof AuthenticatedSystemSettingsAuthSectionRoute
+  AuthenticatedSystemSettingsBillingSectionRoute: typeof AuthenticatedSystemSettingsBillingSectionRoute
   AuthenticatedSystemSettingsContentSectionRoute: typeof AuthenticatedSystemSettingsContentSectionRoute
   AuthenticatedSystemSettingsContentSectionRoute: typeof AuthenticatedSystemSettingsContentSectionRoute
-  AuthenticatedSystemSettingsGeneralSectionRoute: typeof AuthenticatedSystemSettingsGeneralSectionRoute
-  AuthenticatedSystemSettingsIntegrationsSectionRoute: typeof AuthenticatedSystemSettingsIntegrationsSectionRoute
-  AuthenticatedSystemSettingsMaintenanceSectionRoute: typeof AuthenticatedSystemSettingsMaintenanceSectionRoute
   AuthenticatedSystemSettingsModelsSectionRoute: typeof AuthenticatedSystemSettingsModelsSectionRoute
   AuthenticatedSystemSettingsModelsSectionRoute: typeof AuthenticatedSystemSettingsModelsSectionRoute
-  AuthenticatedSystemSettingsRequestLimitsSectionRoute: typeof AuthenticatedSystemSettingsRequestLimitsSectionRoute
+  AuthenticatedSystemSettingsOperationsSectionRoute: typeof AuthenticatedSystemSettingsOperationsSectionRoute
+  AuthenticatedSystemSettingsSecuritySectionRoute: typeof AuthenticatedSystemSettingsSecuritySectionRoute
+  AuthenticatedSystemSettingsSiteSectionRoute: typeof AuthenticatedSystemSettingsSiteSectionRoute
   AuthenticatedSystemSettingsAuthIndexRoute: typeof AuthenticatedSystemSettingsAuthIndexRoute
   AuthenticatedSystemSettingsAuthIndexRoute: typeof AuthenticatedSystemSettingsAuthIndexRoute
+  AuthenticatedSystemSettingsBillingIndexRoute: typeof AuthenticatedSystemSettingsBillingIndexRoute
   AuthenticatedSystemSettingsContentIndexRoute: typeof AuthenticatedSystemSettingsContentIndexRoute
   AuthenticatedSystemSettingsContentIndexRoute: typeof AuthenticatedSystemSettingsContentIndexRoute
-  AuthenticatedSystemSettingsGeneralIndexRoute: typeof AuthenticatedSystemSettingsGeneralIndexRoute
-  AuthenticatedSystemSettingsIntegrationsIndexRoute: typeof AuthenticatedSystemSettingsIntegrationsIndexRoute
-  AuthenticatedSystemSettingsMaintenanceIndexRoute: typeof AuthenticatedSystemSettingsMaintenanceIndexRoute
   AuthenticatedSystemSettingsModelsIndexRoute: typeof AuthenticatedSystemSettingsModelsIndexRoute
   AuthenticatedSystemSettingsModelsIndexRoute: typeof AuthenticatedSystemSettingsModelsIndexRoute
-  AuthenticatedSystemSettingsRequestLimitsIndexRoute: typeof AuthenticatedSystemSettingsRequestLimitsIndexRoute
+  AuthenticatedSystemSettingsOperationsIndexRoute: typeof AuthenticatedSystemSettingsOperationsIndexRoute
+  AuthenticatedSystemSettingsSecurityIndexRoute: typeof AuthenticatedSystemSettingsSecurityIndexRoute
+  AuthenticatedSystemSettingsSiteIndexRoute: typeof AuthenticatedSystemSettingsSiteIndexRoute
 }
 }
 
 
 const AuthenticatedSystemSettingsRouteRouteChildren: AuthenticatedSystemSettingsRouteRouteChildren =
 const AuthenticatedSystemSettingsRouteRouteChildren: AuthenticatedSystemSettingsRouteRouteChildren =
@@ -1180,32 +1180,32 @@ const AuthenticatedSystemSettingsRouteRouteChildren: AuthenticatedSystemSettings
       AuthenticatedSystemSettingsIndexRoute,
       AuthenticatedSystemSettingsIndexRoute,
     AuthenticatedSystemSettingsAuthSectionRoute:
     AuthenticatedSystemSettingsAuthSectionRoute:
       AuthenticatedSystemSettingsAuthSectionRoute,
       AuthenticatedSystemSettingsAuthSectionRoute,
+    AuthenticatedSystemSettingsBillingSectionRoute:
+      AuthenticatedSystemSettingsBillingSectionRoute,
     AuthenticatedSystemSettingsContentSectionRoute:
     AuthenticatedSystemSettingsContentSectionRoute:
       AuthenticatedSystemSettingsContentSectionRoute,
       AuthenticatedSystemSettingsContentSectionRoute,
-    AuthenticatedSystemSettingsGeneralSectionRoute:
-      AuthenticatedSystemSettingsGeneralSectionRoute,
-    AuthenticatedSystemSettingsIntegrationsSectionRoute:
-      AuthenticatedSystemSettingsIntegrationsSectionRoute,
-    AuthenticatedSystemSettingsMaintenanceSectionRoute:
-      AuthenticatedSystemSettingsMaintenanceSectionRoute,
     AuthenticatedSystemSettingsModelsSectionRoute:
     AuthenticatedSystemSettingsModelsSectionRoute:
       AuthenticatedSystemSettingsModelsSectionRoute,
       AuthenticatedSystemSettingsModelsSectionRoute,
-    AuthenticatedSystemSettingsRequestLimitsSectionRoute:
-      AuthenticatedSystemSettingsRequestLimitsSectionRoute,
+    AuthenticatedSystemSettingsOperationsSectionRoute:
+      AuthenticatedSystemSettingsOperationsSectionRoute,
+    AuthenticatedSystemSettingsSecuritySectionRoute:
+      AuthenticatedSystemSettingsSecuritySectionRoute,
+    AuthenticatedSystemSettingsSiteSectionRoute:
+      AuthenticatedSystemSettingsSiteSectionRoute,
     AuthenticatedSystemSettingsAuthIndexRoute:
     AuthenticatedSystemSettingsAuthIndexRoute:
       AuthenticatedSystemSettingsAuthIndexRoute,
       AuthenticatedSystemSettingsAuthIndexRoute,
+    AuthenticatedSystemSettingsBillingIndexRoute:
+      AuthenticatedSystemSettingsBillingIndexRoute,
     AuthenticatedSystemSettingsContentIndexRoute:
     AuthenticatedSystemSettingsContentIndexRoute:
       AuthenticatedSystemSettingsContentIndexRoute,
       AuthenticatedSystemSettingsContentIndexRoute,
-    AuthenticatedSystemSettingsGeneralIndexRoute:
-      AuthenticatedSystemSettingsGeneralIndexRoute,
-    AuthenticatedSystemSettingsIntegrationsIndexRoute:
-      AuthenticatedSystemSettingsIntegrationsIndexRoute,
-    AuthenticatedSystemSettingsMaintenanceIndexRoute:
-      AuthenticatedSystemSettingsMaintenanceIndexRoute,
     AuthenticatedSystemSettingsModelsIndexRoute:
     AuthenticatedSystemSettingsModelsIndexRoute:
       AuthenticatedSystemSettingsModelsIndexRoute,
       AuthenticatedSystemSettingsModelsIndexRoute,
-    AuthenticatedSystemSettingsRequestLimitsIndexRoute:
-      AuthenticatedSystemSettingsRequestLimitsIndexRoute,
+    AuthenticatedSystemSettingsOperationsIndexRoute:
+      AuthenticatedSystemSettingsOperationsIndexRoute,
+    AuthenticatedSystemSettingsSecurityIndexRoute:
+      AuthenticatedSystemSettingsSecurityIndexRoute,
+    AuthenticatedSystemSettingsSiteIndexRoute:
+      AuthenticatedSystemSettingsSiteIndexRoute,
   }
   }
 
 
 const AuthenticatedSystemSettingsRouteRouteWithChildren =
 const AuthenticatedSystemSettingsRouteRouteWithChildren =

+ 21 - 0
web/default/src/routes/_authenticated/system-settings/billing/$section.tsx

@@ -0,0 +1,21 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { BillingSettings } from '@/features/system-settings/billing'
+import {
+  BILLING_DEFAULT_SECTION,
+  BILLING_SECTION_IDS,
+} from '@/features/system-settings/billing/section-registry.tsx'
+
+export const Route = createFileRoute(
+  '/_authenticated/system-settings/billing/$section'
+)({
+  beforeLoad: ({ params }) => {
+    const validSections = BILLING_SECTION_IDS as unknown as string[]
+    if (!validSections.includes(params.section)) {
+      throw redirect({
+        to: '/system-settings/billing/$section',
+        params: { section: BILLING_DEFAULT_SECTION },
+      })
+    }
+  },
+  component: BillingSettings,
+})

+ 13 - 0
web/default/src/routes/_authenticated/system-settings/billing/index.tsx

@@ -0,0 +1,13 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { BILLING_DEFAULT_SECTION } from '@/features/system-settings/billing/section-registry.tsx'
+
+export const Route = createFileRoute(
+  '/_authenticated/system-settings/billing/'
+)({
+  beforeLoad: () => {
+    throw redirect({
+      to: '/system-settings/billing/$section',
+      params: { section: BILLING_DEFAULT_SECTION },
+    })
+  },
+})

+ 0 - 21
web/default/src/routes/_authenticated/system-settings/general/$section.tsx

@@ -1,21 +0,0 @@
-import { createFileRoute, redirect } from '@tanstack/react-router'
-import { GeneralSettings } from '@/features/system-settings/general'
-import {
-  GENERAL_DEFAULT_SECTION,
-  GENERAL_SECTION_IDS,
-} from '@/features/system-settings/general/section-registry.tsx'
-
-export const Route = createFileRoute(
-  '/_authenticated/system-settings/general/$section'
-)({
-  beforeLoad: ({ params }) => {
-    const validSections = GENERAL_SECTION_IDS as unknown as string[]
-    if (!validSections.includes(params.section)) {
-      throw redirect({
-        to: '/system-settings/general/$section',
-        params: { section: GENERAL_DEFAULT_SECTION },
-      })
-    }
-  },
-  component: GeneralSettings,
-})

+ 0 - 13
web/default/src/routes/_authenticated/system-settings/general/index.tsx

@@ -1,13 +0,0 @@
-import { createFileRoute, redirect } from '@tanstack/react-router'
-import { GENERAL_DEFAULT_SECTION } from '@/features/system-settings/general/section-registry.tsx'
-
-export const Route = createFileRoute(
-  '/_authenticated/system-settings/general/'
-)({
-  beforeLoad: () => {
-    throw redirect({
-      to: '/system-settings/general/$section',
-      params: { section: GENERAL_DEFAULT_SECTION },
-    })
-  },
-})

+ 1 - 1
web/default/src/routes/_authenticated/system-settings/index.tsx

@@ -3,7 +3,7 @@ import { createFileRoute, redirect } from '@tanstack/react-router'
 export const Route = createFileRoute('/_authenticated/system-settings/')({
 export const Route = createFileRoute('/_authenticated/system-settings/')({
   beforeLoad: () => {
   beforeLoad: () => {
     throw redirect({
     throw redirect({
-      to: '/system-settings/general',
+      to: '/system-settings/site',
     })
     })
   },
   },
 })
 })

+ 0 - 21
web/default/src/routes/_authenticated/system-settings/integrations/$section.tsx

@@ -1,21 +0,0 @@
-import { createFileRoute, redirect } from '@tanstack/react-router'
-import { IntegrationSettings } from '@/features/system-settings/integrations'
-import {
-  INTEGRATIONS_DEFAULT_SECTION,
-  INTEGRATIONS_SECTION_IDS,
-} from '@/features/system-settings/integrations/section-registry.tsx'
-
-export const Route = createFileRoute(
-  '/_authenticated/system-settings/integrations/$section'
-)({
-  beforeLoad: ({ params }) => {
-    const validSections = INTEGRATIONS_SECTION_IDS as unknown as string[]
-    if (!validSections.includes(params.section)) {
-      throw redirect({
-        to: '/system-settings/integrations/$section',
-        params: { section: INTEGRATIONS_DEFAULT_SECTION },
-      })
-    }
-  },
-  component: IntegrationSettings,
-})

+ 0 - 13
web/default/src/routes/_authenticated/system-settings/integrations/index.tsx

@@ -1,13 +0,0 @@
-import { createFileRoute, redirect } from '@tanstack/react-router'
-import { INTEGRATIONS_DEFAULT_SECTION } from '@/features/system-settings/integrations/section-registry.tsx'
-
-export const Route = createFileRoute(
-  '/_authenticated/system-settings/integrations/'
-)({
-  beforeLoad: () => {
-    throw redirect({
-      to: '/system-settings/integrations/$section',
-      params: { section: INTEGRATIONS_DEFAULT_SECTION },
-    })
-  },
-})

+ 0 - 21
web/default/src/routes/_authenticated/system-settings/maintenance/$section.tsx

@@ -1,21 +0,0 @@
-import { createFileRoute, redirect } from '@tanstack/react-router'
-import { MaintenanceSettings } from '@/features/system-settings/maintenance'
-import {
-  MAINTENANCE_DEFAULT_SECTION,
-  MAINTENANCE_SECTION_IDS,
-} from '@/features/system-settings/maintenance/section-registry.tsx'
-
-export const Route = createFileRoute(
-  '/_authenticated/system-settings/maintenance/$section'
-)({
-  beforeLoad: ({ params }) => {
-    const validSections = MAINTENANCE_SECTION_IDS as unknown as string[]
-    if (!validSections.includes(params.section)) {
-      throw redirect({
-        to: '/system-settings/maintenance/$section',
-        params: { section: MAINTENANCE_DEFAULT_SECTION },
-      })
-    }
-  },
-  component: MaintenanceSettings,
-})

+ 0 - 13
web/default/src/routes/_authenticated/system-settings/maintenance/index.tsx

@@ -1,13 +0,0 @@
-import { createFileRoute, redirect } from '@tanstack/react-router'
-import { MAINTENANCE_DEFAULT_SECTION } from '@/features/system-settings/maintenance/section-registry.tsx'
-
-export const Route = createFileRoute(
-  '/_authenticated/system-settings/maintenance/'
-)({
-  beforeLoad: () => {
-    throw redirect({
-      to: '/system-settings/maintenance/$section',
-      params: { section: MAINTENANCE_DEFAULT_SECTION },
-    })
-  },
-})

+ 21 - 0
web/default/src/routes/_authenticated/system-settings/operations/$section.tsx

@@ -0,0 +1,21 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { OperationsSettings } from '@/features/system-settings/operations'
+import {
+  OPERATIONS_DEFAULT_SECTION,
+  OPERATIONS_SECTION_IDS,
+} from '@/features/system-settings/operations/section-registry.tsx'
+
+export const Route = createFileRoute(
+  '/_authenticated/system-settings/operations/$section'
+)({
+  beforeLoad: ({ params }) => {
+    const validSections = OPERATIONS_SECTION_IDS as unknown as string[]
+    if (!validSections.includes(params.section)) {
+      throw redirect({
+        to: '/system-settings/operations/$section',
+        params: { section: OPERATIONS_DEFAULT_SECTION },
+      })
+    }
+  },
+  component: OperationsSettings,
+})

+ 13 - 0
web/default/src/routes/_authenticated/system-settings/operations/index.tsx

@@ -0,0 +1,13 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { OPERATIONS_DEFAULT_SECTION } from '@/features/system-settings/operations/section-registry.tsx'
+
+export const Route = createFileRoute(
+  '/_authenticated/system-settings/operations/'
+)({
+  beforeLoad: () => {
+    throw redirect({
+      to: '/system-settings/operations/$section',
+      params: { section: OPERATIONS_DEFAULT_SECTION },
+    })
+  },
+})

+ 0 - 21
web/default/src/routes/_authenticated/system-settings/request-limits/$section.tsx

@@ -1,21 +0,0 @@
-import { createFileRoute, redirect } from '@tanstack/react-router'
-import { RequestLimitsSettings } from '@/features/system-settings/request-limits'
-import {
-  REQUEST_LIMITS_DEFAULT_SECTION,
-  REQUEST_LIMITS_SECTION_IDS,
-} from '@/features/system-settings/request-limits/section-registry.tsx'
-
-export const Route = createFileRoute(
-  '/_authenticated/system-settings/request-limits/$section'
-)({
-  beforeLoad: ({ params }) => {
-    const validSections = REQUEST_LIMITS_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 },
-      })
-    }
-  },
-  component: RequestLimitsSettings,
-})

+ 0 - 13
web/default/src/routes/_authenticated/system-settings/request-limits/index.tsx

@@ -1,13 +0,0 @@
-import { createFileRoute, redirect } from '@tanstack/react-router'
-import { REQUEST_LIMITS_DEFAULT_SECTION } from '@/features/system-settings/request-limits/section-registry.tsx'
-
-export const Route = createFileRoute(
-  '/_authenticated/system-settings/request-limits/'
-)({
-  beforeLoad: () => {
-    throw redirect({
-      to: '/system-settings/request-limits/$section',
-      params: { section: REQUEST_LIMITS_DEFAULT_SECTION },
-    })
-  },
-})

+ 21 - 0
web/default/src/routes/_authenticated/system-settings/security/$section.tsx

@@ -0,0 +1,21 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { SecuritySettings } from '@/features/system-settings/security'
+import {
+  SECURITY_DEFAULT_SECTION,
+  SECURITY_SECTION_IDS,
+} from '@/features/system-settings/security/section-registry.tsx'
+
+export const Route = createFileRoute(
+  '/_authenticated/system-settings/security/$section'
+)({
+  beforeLoad: ({ params }) => {
+    const validSections = SECURITY_SECTION_IDS as unknown as string[]
+    if (!validSections.includes(params.section)) {
+      throw redirect({
+        to: '/system-settings/security/$section',
+        params: { section: SECURITY_DEFAULT_SECTION },
+      })
+    }
+  },
+  component: SecuritySettings,
+})

+ 13 - 0
web/default/src/routes/_authenticated/system-settings/security/index.tsx

@@ -0,0 +1,13 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { SECURITY_DEFAULT_SECTION } from '@/features/system-settings/security/section-registry.tsx'
+
+export const Route = createFileRoute(
+  '/_authenticated/system-settings/security/'
+)({
+  beforeLoad: () => {
+    throw redirect({
+      to: '/system-settings/security/$section',
+      params: { section: SECURITY_DEFAULT_SECTION },
+    })
+  },
+})

+ 21 - 0
web/default/src/routes/_authenticated/system-settings/site/$section.tsx

@@ -0,0 +1,21 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { SiteSettings } from '@/features/system-settings/site'
+import {
+  SITE_DEFAULT_SECTION,
+  SITE_SECTION_IDS,
+} from '@/features/system-settings/site/section-registry.tsx'
+
+export const Route = createFileRoute(
+  '/_authenticated/system-settings/site/$section'
+)({
+  beforeLoad: ({ params }) => {
+    const validSections = SITE_SECTION_IDS as unknown as string[]
+    if (!validSections.includes(params.section)) {
+      throw redirect({
+        to: '/system-settings/site/$section',
+        params: { section: SITE_DEFAULT_SECTION },
+      })
+    }
+  },
+  component: SiteSettings,
+})

+ 11 - 0
web/default/src/routes/_authenticated/system-settings/site/index.tsx

@@ -0,0 +1,11 @@
+import { createFileRoute, redirect } from '@tanstack/react-router'
+import { SITE_DEFAULT_SECTION } from '@/features/system-settings/site/section-registry.tsx'
+
+export const Route = createFileRoute('/_authenticated/system-settings/site/')({
+  beforeLoad: () => {
+    throw redirect({
+      to: '/system-settings/site/$section',
+      params: { section: SITE_DEFAULT_SECTION },
+    })
+  },
+})