Commit dede1e29 by CaIon

fix(default): improve billing settings forms

parent 446a8420
...@@ -51,10 +51,10 @@ export function ProfileSettingsCard({ ...@@ -51,10 +51,10 @@ export function ProfileSettingsCard({
icon={<Settings className='h-4 w-4' />} icon={<Settings className='h-4 w-4' />}
> >
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className='grid h-auto w-full grid-cols-2 gap-1 rounded-xl p-1'> <TabsList className='grid h-10 w-full grid-cols-2 items-stretch gap-1 rounded-xl p-1'>
<TabsTrigger <TabsTrigger
value='bindings' value='bindings'
className='h-auto gap-2 rounded-lg px-3 py-2' className='h-full gap-2 rounded-lg px-3 py-0 leading-none'
> >
<Link2 className='h-4 w-4' /> <Link2 className='h-4 w-4' />
<span className='hidden sm:inline'>{t('Account Bindings')}</span> <span className='hidden sm:inline'>{t('Account Bindings')}</span>
...@@ -62,7 +62,7 @@ export function ProfileSettingsCard({ ...@@ -62,7 +62,7 @@ export function ProfileSettingsCard({
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value='settings' value='settings'
className='h-auto gap-2 rounded-lg px-3 py-2' className='h-full gap-2 rounded-lg px-3 py-0 leading-none'
> >
<Settings className='h-4 w-4' /> <Settings className='h-4 w-4' />
<span className='hidden sm:inline'> <span className='hidden sm:inline'>
......
...@@ -45,9 +45,13 @@ const BILLING_SECTIONS = [ ...@@ -45,9 +45,13 @@ const BILLING_SECTIONS = [
QuotaForInviter: settings.QuotaForInviter, QuotaForInviter: settings.QuotaForInviter,
QuotaForInvitee: settings.QuotaForInvitee, QuotaForInvitee: settings.QuotaForInvitee,
TopUpLink: settings.TopUpLink, TopUpLink: settings.TopUpLink,
'general_setting.docs_link': settings['general_setting.docs_link'], general_setting: {
'quota_setting.enable_free_model_pre_consume': docs_link: settings['general_setting.docs_link'],
settings['quota_setting.enable_free_model_pre_consume'], },
quota_setting: {
enable_free_model_pre_consume:
settings['quota_setting.enable_free_model_pre_consume'],
},
}} }}
/> />
), ),
......
import * as z from 'zod' import * as z from 'zod'
import type { ChangeEvent } from 'react'
import type { Resolver } from 'react-hook-form' import type { Resolver } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
...@@ -25,9 +26,13 @@ const quotaSchema = z.object({ ...@@ -25,9 +26,13 @@ const quotaSchema = z.object({
PreConsumedQuota: z.coerce.number().min(0), PreConsumedQuota: z.coerce.number().min(0),
QuotaForInviter: z.coerce.number().min(0), QuotaForInviter: z.coerce.number().min(0),
QuotaForInvitee: z.coerce.number().min(0), QuotaForInvitee: z.coerce.number().min(0),
TopUpLink: z.string().url().optional().or(z.literal('')), TopUpLink: z.string(),
'general_setting.docs_link': z.string().url().optional().or(z.literal('')), general_setting: z.object({
'quota_setting.enable_free_model_pre_consume': z.boolean(), docs_link: z.string(),
}),
quota_setting: z.object({
enable_free_model_pre_consume: z.boolean(),
}),
}) })
type QuotaFormValues = z.infer<typeof quotaSchema> type QuotaFormValues = z.infer<typeof quotaSchema>
...@@ -41,6 +46,13 @@ export function QuotaSettingsSection({ ...@@ -41,6 +46,13 @@ export function QuotaSettingsSection({
}: QuotaSettingsSectionProps) { }: QuotaSettingsSectionProps) {
const { t } = useTranslation() const { t } = useTranslation()
const updateOption = useUpdateOption() const updateOption = useUpdateOption()
const handleNumberChange =
(onChange: (value: number | string) => void) =>
(event: ChangeEvent<HTMLInputElement>) => {
onChange(
event.target.value === '' ? '' : event.currentTarget.valueAsNumber
)
}
const { form, handleSubmit, isDirty, isSubmitting } = const { form, handleSubmit, isDirty, isSubmitting } =
useSettingsForm<QuotaFormValues>({ useSettingsForm<QuotaFormValues>({
...@@ -79,8 +91,8 @@ export function QuotaSettingsSection({ ...@@ -79,8 +91,8 @@ export function QuotaSettingsSection({
<FormControl> <FormControl>
<Input <Input
type='number' type='number'
value={field.value as number} value={field.value ?? ''}
onChange={(e) => field.onChange(e.target.valueAsNumber)} onChange={handleNumberChange(field.onChange)}
name={field.name} name={field.name}
onBlur={field.onBlur} onBlur={field.onBlur}
ref={field.ref} ref={field.ref}
...@@ -103,8 +115,8 @@ export function QuotaSettingsSection({ ...@@ -103,8 +115,8 @@ export function QuotaSettingsSection({
<FormControl> <FormControl>
<Input <Input
type='number' type='number'
value={field.value as number} value={field.value ?? ''}
onChange={(e) => field.onChange(e.target.valueAsNumber)} onChange={handleNumberChange(field.onChange)}
name={field.name} name={field.name}
onBlur={field.onBlur} onBlur={field.onBlur}
ref={field.ref} ref={field.ref}
...@@ -127,8 +139,8 @@ export function QuotaSettingsSection({ ...@@ -127,8 +139,8 @@ export function QuotaSettingsSection({
<FormControl> <FormControl>
<Input <Input
type='number' type='number'
value={field.value as number} value={field.value ?? ''}
onChange={(e) => field.onChange(e.target.valueAsNumber)} onChange={handleNumberChange(field.onChange)}
name={field.name} name={field.name}
onBlur={field.onBlur} onBlur={field.onBlur}
ref={field.ref} ref={field.ref}
...@@ -151,8 +163,8 @@ export function QuotaSettingsSection({ ...@@ -151,8 +163,8 @@ export function QuotaSettingsSection({
<FormControl> <FormControl>
<Input <Input
type='number' type='number'
value={field.value as number} value={field.value ?? ''}
onChange={(e) => field.onChange(e.target.valueAsNumber)} onChange={handleNumberChange(field.onChange)}
name={field.name} name={field.name}
onBlur={field.onBlur} onBlur={field.onBlur}
ref={field.ref} ref={field.ref}
......
import { memo, useCallback, useState } from 'react' import { memo, useCallback, useState } from 'react'
import { type UseFormReturn } from 'react-hook-form' import { type UseFormReturn } from 'react-hook-form'
import { Code2, Eye } from 'lucide-react' import { Code2, Eye, HelpCircle } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion'
import {
Form, Form,
FormControl, FormControl,
FormDescription, FormDescription,
...@@ -14,6 +20,13 @@ import { ...@@ -14,6 +20,13 @@ import {
} from '@/components/ui/form' } from '@/components/ui/form'
import { Switch } from '@/components/ui/switch' import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet'
import { GroupRatioVisualEditor } from './group-ratio-visual-editor' import { GroupRatioVisualEditor } from './group-ratio-visual-editor'
import { GroupSpecialUsableRulesEditor } from './group-special-usable-editor' import { GroupSpecialUsableRulesEditor } from './group-special-usable-editor'
...@@ -40,6 +53,7 @@ export const GroupRatioForm = memo(function GroupRatioForm({ ...@@ -40,6 +53,7 @@ export const GroupRatioForm = memo(function GroupRatioForm({
}: GroupRatioFormProps) { }: GroupRatioFormProps) {
const { t } = useTranslation() const { t } = useTranslation()
const [editMode, setEditMode] = useState<'visual' | 'json'>('visual') const [editMode, setEditMode] = useState<'visual' | 'json'>('visual')
const [guideOpen, setGuideOpen] = useState(false)
const handleFieldChange = useCallback( const handleFieldChange = useCallback(
(field: keyof GroupFormValues, value: string) => { (field: keyof GroupFormValues, value: string) => {
...@@ -57,7 +71,15 @@ export const GroupRatioForm = memo(function GroupRatioForm({ ...@@ -57,7 +71,15 @@ export const GroupRatioForm = memo(function GroupRatioForm({
return ( return (
<div className='space-y-6'> <div className='space-y-6'>
<div className='flex justify-end'> <div className='flex flex-wrap justify-end gap-2'>
<Button
variant='outline'
size='sm'
onClick={() => setGuideOpen(true)}
>
<HelpCircle className='mr-2 h-4 w-4' />
{t('Usage guide')}
</Button>
<Button variant='outline' size='sm' onClick={toggleEditMode}> <Button variant='outline' size='sm' onClick={toggleEditMode}>
{editMode === 'visual' ? ( {editMode === 'visual' ? (
<> <>
...@@ -73,6 +95,8 @@ export const GroupRatioForm = memo(function GroupRatioForm({ ...@@ -73,6 +95,8 @@ export const GroupRatioForm = memo(function GroupRatioForm({
</Button> </Button>
</div> </div>
<GroupPricingGuide open={guideOpen} onOpenChange={setGuideOpen} />
<Form {...form}> <Form {...form}>
{editMode === 'visual' ? ( {editMode === 'visual' ? (
<div className='space-y-6'> <div className='space-y-6'>
...@@ -276,3 +300,165 @@ export const GroupRatioForm = memo(function GroupRatioForm({ ...@@ -276,3 +300,165 @@ export const GroupRatioForm = memo(function GroupRatioForm({
</div> </div>
) )
}) })
type GroupPricingGuideProps = {
open: boolean
onOpenChange: (open: boolean) => void
}
function GuideCodeBlock({ children }: { children: string }) {
return (
<pre className='bg-muted/60 overflow-x-auto rounded-lg border px-3 py-2 text-xs leading-6 whitespace-pre-wrap'>
{children}
</pre>
)
}
function GroupPricingGuide({ open, onOpenChange }: GroupPricingGuideProps) {
const { t } = useTranslation()
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent side='right' className='w-full gap-0 p-0 sm:max-w-2xl'>
<SheetHeader className='border-b p-4'>
<SheetTitle>{t('Group pricing usage guide')}</SheetTitle>
<SheetDescription>
{t(
'Understand how user groups, token groups, ratios, and special rules work together.'
)}
</SheetDescription>
</SheetHeader>
<div className='space-y-5 overflow-y-auto p-4'>
<section className='space-y-2'>
<h3 className='text-sm font-semibold'>{t('Core concepts')}</h3>
<div className='text-muted-foreground space-y-2 text-sm leading-6'>
<p>
<span className='text-foreground font-medium'>
{t('User group')}
</span>
{': '}
{t(
'Assigned by administrators and used to represent a user level, such as default or vip.'
)}
</p>
<p>
<span className='text-foreground font-medium'>
{t('Token group')}
</span>
{': '}
{t(
'Selected when creating a token and used as the default billing group for API calls.'
)}
</p>
<p>
<span className='text-foreground font-medium'>
{t('Ratio')}
</span>
{': '}
{t(
'A billing multiplier. Lower ratios mean lower API call costs.'
)}
</p>
<p>
<span className='text-foreground font-medium'>
{t('User selectable')}
</span>
{': '}
{t(
'When enabled, users can pick this group when creating tokens.'
)}
</p>
</div>
</section>
<Accordion className='rounded-lg border px-3'>
<AccordionItem value='groups'>
<AccordionTrigger>{t('Pricing group example')}</AccordionTrigger>
<AccordionContent className='space-y-3'>
<p className='text-muted-foreground text-sm leading-6'>
{t(
'Use the pricing group table to manage the ratio and whether the group appears in the token creation dropdown.'
)}
</p>
<GuideCodeBlock>
{`${t('Group name')} ${t('Ratio')} ${t('User selectable')} ${t('Description')}
standard 1.0 ${t('Yes')} ${t('Standard price')}
premium 0.5 ${t('Yes')} ${t('Premium plan, half price')}
vip 0.5 ${t('No')} ${t('Assigned by administrator only')}`}
</GuideCodeBlock>
<p className='text-muted-foreground text-sm leading-6'>
{t(
'Users only see groups marked as user selectable. Non-selectable groups can still be assigned by administrators.'
)}
</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value='auto'>
<AccordionTrigger>{t('Auto group behavior')}</AccordionTrigger>
<AccordionContent className='space-y-3'>
<p className='text-muted-foreground text-sm leading-6'>
{t(
'When a token uses the auto group, the system tries groups from top to bottom until it finds an available group.'
)}
</p>
<GuideCodeBlock>{`["default", "vip"]`}</GuideCodeBlock>
<p className='text-muted-foreground text-sm leading-6'>
{t(
'If default auto group is enabled, newly created tokens start with auto instead of an empty group.'
)}
</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value='special-ratio'>
<AccordionTrigger>{t('Special ratio rules')}</AccordionTrigger>
<AccordionContent className='space-y-3'>
<p className='text-muted-foreground text-sm leading-6'>
{t(
'Special ratios override the token group ratio for specific user group and token group combinations.'
)}
</p>
<GuideCodeBlock>{`{
"vip": {
"standard": 0.8,
"premium": 0.3
}
}`}</GuideCodeBlock>
<p className='text-muted-foreground text-sm leading-6'>
{t(
'Only configured combinations are overridden. All other calls keep the token group base ratio.'
)}
</p>
</AccordionContent>
</AccordionItem>
<AccordionItem value='usable'>
<AccordionTrigger>{t('Special usable group rules')}</AccordionTrigger>
<AccordionContent className='space-y-3'>
<p className='text-muted-foreground text-sm leading-6'>
{t(
'Special usable group rules can add, remove, or append selectable token groups for a specific user group.'
)}
</p>
<GuideCodeBlock>{`{
"vip": {
"+:premium": "${t('Premium plan, half price')}",
"-:default": "remove",
"special": "${t('Special group')}"
}
}`}</GuideCodeBlock>
<p className='text-muted-foreground text-sm leading-6'>
{t(
'Use +: to add a group, -: to remove a default selectable group, or no prefix to append a group.'
)}
</p>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</SheetContent>
</Sheet>
)
}
...@@ -27,6 +27,9 @@ import { StatusBadge } from '@/components/status-badge' ...@@ -27,6 +27,9 @@ import { StatusBadge } from '@/components/status-badge'
const OP_ADD = 'add' as const const OP_ADD = 'add' as const
const OP_REMOVE = 'remove' as const const OP_REMOVE = 'remove' as const
const OP_APPEND = 'append' as const const OP_APPEND = 'append' as const
const sectionCardClassName =
'relative shadow-sm ring-0 before:pointer-events-none before:absolute before:inset-0 before:rounded-xl before:border before:border-border/90'
const sectionHeaderClassName = 'border-b bg-muted/20'
type OpType = typeof OP_ADD | typeof OP_REMOVE | typeof OP_APPEND type OpType = typeof OP_ADD | typeof OP_REMOVE | typeof OP_APPEND
...@@ -344,8 +347,8 @@ export function GroupSpecialUsableRulesEditor( ...@@ -344,8 +347,8 @@ export function GroupSpecialUsableRulesEditor(
}, [rules]) }, [rules])
return ( return (
<Card> <Card className={sectionCardClassName}>
<CardHeader> <CardHeader className={sectionHeaderClassName}>
<CardTitle>{t('Special usable group rules')}</CardTitle> <CardTitle>{t('Special usable group rules')}</CardTitle>
<CardDescription> <CardDescription>
{t( {t(
......
...@@ -132,7 +132,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({ ...@@ -132,7 +132,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({
<div className='flex flex-wrap gap-4'> <div className='flex flex-wrap gap-4'>
<Button onClick={form.handleSubmit(onSave)} disabled={isSaving}> <Button onClick={form.handleSubmit(onSave)} disabled={isSaving}>
{isSaving ? t('Saving...') : t('Save model ratios')} {isSaving ? t('Saving...') : t('Save model prices')}
</Button> </Button>
<Button <Button
type='button' type='button'
...@@ -140,7 +140,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({ ...@@ -140,7 +140,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({
onClick={onReset} onClick={onReset}
disabled={isResetting} disabled={isResetting}
> >
{t('Reset ratios')} {t('Reset prices')}
</Button> </Button>
</div> </div>
</div> </div>
...@@ -323,7 +323,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({ ...@@ -323,7 +323,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({
<div className='flex flex-wrap gap-4'> <div className='flex flex-wrap gap-4'>
<Button type='submit' disabled={isSaving}> <Button type='submit' disabled={isSaving}>
{isSaving ? t('Saving...') : t('Save model ratios')} {isSaving ? t('Saving...') : t('Save model prices')}
</Button> </Button>
<Button <Button
type='button' type='button'
...@@ -331,7 +331,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({ ...@@ -331,7 +331,7 @@ export const ModelRatioForm = memo(function ModelRatioForm({
onClick={onReset} onClick={onReset}
disabled={isResetting} disabled={isResetting}
> >
{t('Reset ratios')} {t('Reset prices')}
</Button> </Button>
</div> </div>
</form> </form>
......
...@@ -207,7 +207,7 @@ export function RatioSettingsCard({ ...@@ -207,7 +207,7 @@ export function RatioSettingsCard({
mutationFn: resetModelRatios, mutationFn: resetModelRatios,
onSuccess: (data) => { onSuccess: (data) => {
if (data.success) { if (data.success) {
toast.success(t('Model ratios reset successfully')) toast.success(t('Model prices reset successfully'))
queryClient.invalidateQueries({ queryKey: ['system-options'] }) queryClient.invalidateQueries({ queryKey: ['system-options'] })
setConfirmOpen(false) setConfirmOpen(false)
} else { } else {
...@@ -422,7 +422,7 @@ export function RatioSettingsCard({ ...@@ -422,7 +422,7 @@ export function RatioSettingsCard({
}, [resetMutate]) }, [resetMutate])
const tabLabels: Record<RatioTabId, string> = { const tabLabels: Record<RatioTabId, string> = {
models: 'Model ratios', models: 'Model prices',
groups: 'Group ratios', groups: 'Group ratios',
'tool-prices': 'Tool prices', 'tool-prices': 'Tool prices',
'upstream-sync': 'Upstream price sync', 'upstream-sync': 'Upstream price sync',
...@@ -480,26 +480,30 @@ export function RatioSettingsCard({ ...@@ -480,26 +480,30 @@ export function RatioSettingsCard({
return ( return (
<SettingsSection title={t(titleKey)} description={t(descriptionKey)}> <SettingsSection title={t(titleKey)} description={t(descriptionKey)}>
<Tabs defaultValue={defaultTab} className='space-y-6'> {visibleTabs.length === 1 ? (
<TabsList className={`grid w-full ${tabsGridClass}`}> renderTabContent(defaultTab)
) : (
<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>
{visibleTabs.map((tab) => ( {visibleTabs.map((tab) => (
<TabsTrigger key={tab} value={tab}> <TabsContent key={tab} value={tab}>
{t(tabLabels[tab])} {renderTabContent(tab)}
</TabsTrigger> </TabsContent>
))} ))}
</TabsList> </Tabs>
)}
{visibleTabs.map((tab) => (
<TabsContent key={tab} value={tab}>
{renderTabContent(tab)}
</TabsContent>
))}
</Tabs>
<ConfirmDialog <ConfirmDialog
open={confirmOpen} open={confirmOpen}
onOpenChange={setConfirmOpen} onOpenChange={setConfirmOpen}
title={t('Reset all model ratios?')} title={t('Reset all model prices?')}
desc={t( desc={t(
'This will clear custom pricing ratios and revert to upstream defaults.' 'This will clear custom pricing ratios and revert to upstream defaults.'
)} )}
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
"DeepSeek": "DeepSeek", "DeepSeek": "DeepSeek",
"Discord": "Discord", "Discord": "Discord",
"DoubaoVideo": "DoubaoVideo", "DoubaoVideo": "DoubaoVideo",
"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.",
"edit_this": "edit_this", "edit_this": "edit_this",
"FastGPT": "FastGPT", "FastGPT": "FastGPT",
"footer.columns.related.links.midjourney": "Midjourney-Proxy", "footer.columns.related.links.midjourney": "Midjourney-Proxy",
...@@ -78,7 +79,6 @@ ...@@ -78,7 +79,6 @@
"SunoAPI": "SunoAPI", "SunoAPI": "SunoAPI",
"Telegram": "Telegram", "Telegram": "Telegram",
"Token prices": "Token prices", "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",
......
...@@ -8,19 +8,25 @@ ...@@ -8,19 +8,25 @@
"AccessKey / SecretAccessKey": "AccessKey / SecretAccessKey", "AccessKey / SecretAccessKey": "AccessKey / SecretAccessKey",
"AI Proxy": "AI Proxy", "AI Proxy": "AI Proxy",
"AIGC2D": "AIGC2D", "AIGC2D": "AIGC2D",
"All conditions must match before this tier is used.": "All conditions must match before this tier is used.",
"Anthropic": "Anthropic", "Anthropic": "Anthropic",
"API2GPT": "API2GPT", "API2GPT": "API2GPT",
"AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *", "AZURE_OPENAI_ENDPOINT *": "AZURE_OPENAI_ENDPOINT *",
"Baidu V2": "Baidu V2", "Baidu V2": "Baidu V2",
"Base input and output token prices for this tier.": "Base input and output token prices for this tier.",
"Cache pricing": "Cache pricing",
"checkout.session.completed": "checkout.session.completed", "checkout.session.completed": "checkout.session.completed",
"checkout.session.expired": "checkout.session.expired", "checkout.session.expired": "checkout.session.expired",
"Cloudflare": "Cloudflare", "Cloudflare": "Cloudflare",
"Cohere": "Cohere", "Cohere": "Cohere",
"Core pricing": "Core pricing",
"DeepSeek": "DeepSeek", "DeepSeek": "DeepSeek",
"Discord": "Discord", "Discord": "Discord",
"DoubaoVideo": "DoubaoVideo", "DoubaoVideo": "DoubaoVideo",
"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.",
"example.com&#10;blocked-site.com": "example.com&#10;blocked-site.com", "example.com&#10;blocked-site.com": "example.com&#10;blocked-site.com",
"example.com&#10;company.com": "example.com&#10;company.com", "example.com&#10;company.com": "example.com&#10;company.com",
"Fallback tier": "Fallback tier",
"FastGPT": "FastGPT", "FastGPT": "FastGPT",
"footer.columns.related.links.midjourney": "Midjourney-Proxy", "footer.columns.related.links.midjourney": "Midjourney-Proxy",
"footer.columns.related.links.neko": "neko-api-key-tool", "footer.columns.related.links.neko": "neko-api-key-tool",
...@@ -53,6 +59,7 @@ ...@@ -53,6 +59,7 @@
"JustSong": "JustSong", "JustSong": "JustSong",
"LingYiWanWu": "LingYiWanWu", "LingYiWanWu": "LingYiWanWu",
"LinuxDO": "LinuxDO", "LinuxDO": "LinuxDO",
"Media pricing": "Media pricing",
"Midjourney": "Midjourney", "Midjourney": "Midjourney",
"MidjourneyPlus": "MidjourneyPlus", "MidjourneyPlus": "MidjourneyPlus",
"MiniMax": "MiniMax", "MiniMax": "MiniMax",
...@@ -61,6 +68,7 @@ ...@@ -61,6 +68,7 @@
"Moonshot": "Moonshot", "Moonshot": "Moonshot",
"name@example.com": "name@example.com", "name@example.com": "name@example.com",
"NewAPI": "NewAPI", "NewAPI": "NewAPI",
"No separate media pricing configured.": "No separate media pricing configured.",
"noreply@example.com": "noreply@example.com", "noreply@example.com": "noreply@example.com",
"OAuth Client Secret": "OAuth Client Secret", "OAuth Client Secret": "OAuth Client Secret",
"OhMyGPT": "OhMyGPT", "OhMyGPT": "OhMyGPT",
...@@ -73,6 +81,8 @@ ...@@ -73,6 +81,8 @@
"price_xxx": "price_xxx", "price_xxx": "price_xxx",
"QuantumNous": "QuantumNous", "QuantumNous": "QuantumNous",
"Replicate": "Replicate", "Replicate": "Replicate",
"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.",
"SiliconFlow": "SiliconFlow", "SiliconFlow": "SiliconFlow",
"smtp.example.com": "smtp.example.com", "smtp.example.com": "smtp.example.com",
"socks5://user:pass@host:port": "socks5://user:pass@host:port", "socks5://user:pass@host:port": "socks5://user:pass@host:port",
...@@ -81,8 +91,9 @@ ...@@ -81,8 +91,9 @@
"SunoAPI": "SunoAPI", "SunoAPI": "SunoAPI",
"Telegram": "Telegram", "Telegram": "Telegram",
"Tencent": "Tencent", "Tencent": "Tencent",
"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 prices": "Token prices", "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",
...@@ -92,16 +103,5 @@ ...@@ -92,16 +103,5 @@
"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"
} }
{ {
"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.", "Base input and output token prices for this tier.": "Base input and output token prices for this tier.",
"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.",
"Set separate prices for cache reads and writes.": "Set separate prices for cache reads and writes." "Set separate prices for cache reads and writes.": "Set separate prices for cache reads and writes."
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment