feat: remove Get Started arrow, redesign pricing table with Model/Group/Billing/PerRequest/Input/CachedInput/Output columns, default to table view
Docker Build / Build and Push Docker Image (push) Successful in 4m13s
Docker Build / Build and Push Docker Image (push) Successful in 4m13s
This commit is contained in:
@@ -20,7 +20,6 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowRight,
|
||||
Check,
|
||||
Copy,
|
||||
} from 'lucide-react'
|
||||
@@ -197,20 +196,18 @@ export function Hero(props: HeroProps) {
|
||||
<div className='mt-8 flex items-center justify-center gap-3'>
|
||||
{props.isAuthenticated ? (
|
||||
<Button
|
||||
className='group h-10 rounded-lg px-6 text-sm font-semibold'
|
||||
className='h-10 rounded-lg px-6 text-sm font-semibold'
|
||||
render={<Link to='/dashboard' />}
|
||||
>
|
||||
{t('Dashboard')}
|
||||
<ArrowRight className='ml-1 size-3.5 transition-transform duration-200 group-hover:translate-x-0.5' />
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
className='group h-10 rounded-lg px-6 text-sm font-semibold'
|
||||
className='h-10 rounded-lg px-6 text-sm font-semibold'
|
||||
render={<Link to='/sign-up' />}
|
||||
>
|
||||
{t('Get Started')}
|
||||
<ArrowRight className='ml-1 size-3.5 transition-transform duration-200 group-hover:translate-x-0.5' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
|
||||
+214
-332
@@ -33,7 +33,6 @@ import {
|
||||
getDynamicDisplayGroupRatio,
|
||||
getDynamicPricingSummary,
|
||||
} from '../lib/dynamic-price'
|
||||
import { parseTags } from '../lib/filters'
|
||||
import { isTokenBasedModel } from '../lib/model-helpers'
|
||||
import {
|
||||
formatPrice,
|
||||
@@ -53,22 +52,6 @@ export interface PricingColumnsOptions {
|
||||
showRechargePrice?: boolean
|
||||
}
|
||||
|
||||
function renderLimitedTags(
|
||||
items: string[],
|
||||
maxDisplay: number = 3
|
||||
): React.ReactNode {
|
||||
return (
|
||||
<StatusBadgeList
|
||||
items={items}
|
||||
max={maxDisplay}
|
||||
getKey={(item) => item}
|
||||
renderItem={(item) => (
|
||||
<StatusBadge label={item} autoColor={item} size='sm' copyable={false} />
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function renderLimitedGroupBadges(
|
||||
groups: string[],
|
||||
maxDisplay: number = 2
|
||||
@@ -110,7 +93,7 @@ export function usePricingColumns(
|
||||
const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 14) : null
|
||||
|
||||
return (
|
||||
<div className='flex min-w-[200px] items-center gap-2'>
|
||||
<div className='flex items-center gap-2'>
|
||||
{modelIcon}
|
||||
<span className='truncate font-mono text-sm font-medium'>
|
||||
{model.model_name}
|
||||
@@ -118,323 +101,14 @@ export function usePricingColumns(
|
||||
</div>
|
||||
)
|
||||
},
|
||||
minSize: 200,
|
||||
minSize: 180,
|
||||
},
|
||||
|
||||
// Type column
|
||||
{
|
||||
accessorKey: 'quota_type',
|
||||
meta: { label: t('Type') },
|
||||
header: t('Type'),
|
||||
cell: ({ row }) => {
|
||||
const isTokenBased = row.original.quota_type === QUOTA_TYPE_VALUES.TOKEN
|
||||
return (
|
||||
<StatusBadge
|
||||
label={isTokenBased ? t('Token') : t('Request')}
|
||||
variant={isTokenBased ? 'info' : 'neutral'}
|
||||
copyable={false}
|
||||
/>
|
||||
)
|
||||
},
|
||||
size: 80,
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
// Price column
|
||||
{
|
||||
accessorKey: 'price',
|
||||
meta: { label: t('Price') },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t('Price')} />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const model = row.original
|
||||
const dynamicSummary = getDynamicPricingSummary(model, {
|
||||
tokenUnit,
|
||||
showRechargePrice,
|
||||
priceRate,
|
||||
usdExchangeRate,
|
||||
groupRatioMultiplier: getDynamicDisplayGroupRatio(model),
|
||||
})
|
||||
|
||||
if (dynamicSummary) {
|
||||
if (dynamicSummary.isSpecialExpression) {
|
||||
return (
|
||||
<div className='max-w-[320px] min-w-[200px]'>
|
||||
<div className='text-xs font-medium text-amber-700 dark:text-amber-300'>
|
||||
{t('Special billing expression')}
|
||||
</div>
|
||||
<div className='text-muted-foreground text-[11px]'>
|
||||
{t('Unable to parse structured pricing')}
|
||||
</div>
|
||||
<code className='text-muted-foreground/70 mt-1 line-clamp-2 block font-mono text-[10px] leading-relaxed break-all'>
|
||||
{dynamicSummary.rawExpression}
|
||||
</code>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const primaryEntries = dynamicSummary.primaryEntries.slice(0, 2)
|
||||
if (primaryEntries.length === 0) {
|
||||
return (
|
||||
<span className='text-muted-foreground text-xs'>
|
||||
{t('Dynamic Pricing')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='min-w-[180px]'>
|
||||
<span className='font-mono text-sm tabular-nums'>
|
||||
{primaryEntries.map((entry, index) => (
|
||||
<span key={entry.key}>
|
||||
{index > 0 && (
|
||||
<span className='text-muted-foreground/40 mx-1'>/</span>
|
||||
)}
|
||||
{stripTrailingZeros(entry.formatted)}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<div className='text-muted-foreground/50 text-[10px]'>
|
||||
/ {tokenUnitLabel} tokens
|
||||
{dynamicSummary.tierCount > 1 &&
|
||||
` · ${t('{{count}} tiers', {
|
||||
count: dynamicSummary.tierCount,
|
||||
})}`}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const isTokenBased = isTokenBasedModel(model)
|
||||
|
||||
if (isTokenBased) {
|
||||
const inputPrice = stripTrailingZeros(
|
||||
formatPrice(
|
||||
model,
|
||||
'input',
|
||||
tokenUnit,
|
||||
showRechargePrice,
|
||||
priceRate,
|
||||
usdExchangeRate
|
||||
)
|
||||
)
|
||||
const outputPrice = stripTrailingZeros(
|
||||
formatPrice(
|
||||
model,
|
||||
'output',
|
||||
tokenUnit,
|
||||
showRechargePrice,
|
||||
priceRate,
|
||||
usdExchangeRate
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='min-w-[160px]'>
|
||||
<span className='font-mono text-sm tabular-nums'>
|
||||
{inputPrice}
|
||||
<span className='text-muted-foreground/40 mx-1'>/</span>
|
||||
{outputPrice}
|
||||
</span>
|
||||
<div className='text-muted-foreground/50 text-[10px]'>
|
||||
/ {tokenUnitLabel} tokens
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const price = stripTrailingZeros(
|
||||
formatRequestPrice(
|
||||
model,
|
||||
showRechargePrice,
|
||||
priceRate,
|
||||
usdExchangeRate
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='min-w-[100px]'>
|
||||
<span className='font-mono text-sm tabular-nums'>{price}</span>
|
||||
<div className='text-muted-foreground/50 text-[10px]'>
|
||||
/ {t('request')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
size: 180,
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
// Cached price column (Vercel AI Gateway style)
|
||||
{
|
||||
id: 'cached_price',
|
||||
meta: { label: t('Cached') },
|
||||
header: t('Cached'),
|
||||
cell: ({ row }) => {
|
||||
const model = row.original
|
||||
const dynamicSummary = getDynamicPricingSummary(model, {
|
||||
tokenUnit,
|
||||
showRechargePrice,
|
||||
priceRate,
|
||||
usdExchangeRate,
|
||||
groupRatioMultiplier: getDynamicDisplayGroupRatio(model),
|
||||
})
|
||||
|
||||
if (dynamicSummary) {
|
||||
if (dynamicSummary.isSpecialExpression) {
|
||||
return (
|
||||
<span className='text-muted-foreground/50 text-xs'>
|
||||
{t('Special billing expression')}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const cacheEntry = dynamicSummary.entries.find(
|
||||
(entry) => entry.field === 'cacheReadPrice'
|
||||
)
|
||||
if (!cacheEntry) {
|
||||
return <span className='text-muted-foreground/30 text-xs'>—</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='min-w-[80px]'>
|
||||
<span className='font-mono text-sm tabular-nums'>
|
||||
{stripTrailingZeros(cacheEntry.formatted)}
|
||||
</span>
|
||||
<div className='text-muted-foreground/50 text-[10px]'>
|
||||
/ {tokenUnitLabel}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const isTokenBased = isTokenBasedModel(model)
|
||||
|
||||
if (!isTokenBased || model.cache_ratio == null) {
|
||||
return <span className='text-muted-foreground/30 text-xs'>—</span>
|
||||
}
|
||||
|
||||
const cachedPrice = stripTrailingZeros(
|
||||
formatPrice(
|
||||
model,
|
||||
'cache',
|
||||
tokenUnit,
|
||||
showRechargePrice,
|
||||
priceRate,
|
||||
usdExchangeRate
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='min-w-[80px]'>
|
||||
<span className='font-mono text-sm tabular-nums'>
|
||||
{cachedPrice}
|
||||
</span>
|
||||
<div className='text-muted-foreground/50 text-[10px]'>
|
||||
/ {tokenUnitLabel}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
size: 110,
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
// Vendor column
|
||||
{
|
||||
accessorKey: 'vendor_name',
|
||||
meta: { label: t('Vendor') },
|
||||
header: t('Vendor'),
|
||||
cell: ({ row }) => {
|
||||
const model = row.original
|
||||
if (!model.vendor_name) {
|
||||
return <span className='text-muted-foreground/50 text-xs'>—</span>
|
||||
}
|
||||
const vendorIcon = model.vendor_icon
|
||||
? getLobeIcon(model.vendor_icon, 12)
|
||||
: null
|
||||
return (
|
||||
<span className='flex items-center gap-1.5'>
|
||||
{vendorIcon}
|
||||
<StatusBadge
|
||||
label={model.vendor_name}
|
||||
autoColor={model.vendor_name}
|
||||
size='sm'
|
||||
copyable={false}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
size: 130,
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
// Tags column
|
||||
{
|
||||
accessorKey: 'tags',
|
||||
meta: { label: t('Tags') },
|
||||
header: t('Tags'),
|
||||
cell: ({ row }) => {
|
||||
const tags = parseTags(row.original.tags)
|
||||
if (tags.length === 0) {
|
||||
return <span className='text-muted-foreground/50 text-xs'>—</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div />}>
|
||||
{renderLimitedTags(tags, 2)}
|
||||
</TooltipTrigger>
|
||||
{tags.length > 2 && (
|
||||
<TooltipContent side='top' className='max-w-[280px] p-2'>
|
||||
<span className='text-xs'>{tags.join(', ')}</span>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
},
|
||||
size: 140,
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
// Endpoints column
|
||||
{
|
||||
accessorKey: 'supported_endpoint_types',
|
||||
meta: { label: t('Endpoints') },
|
||||
header: t('Endpoints'),
|
||||
cell: ({ row }) => {
|
||||
const endpoints = row.original.supported_endpoint_types || []
|
||||
if (endpoints.length === 0) {
|
||||
return <span className='text-muted-foreground/50 text-xs'>—</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div />}>
|
||||
{renderLimitedTags(endpoints, 2)}
|
||||
</TooltipTrigger>
|
||||
{endpoints.length > 2 && (
|
||||
<TooltipContent side='top' className='max-w-[280px] p-2'>
|
||||
<span className='text-xs'>{endpoints.join(', ')}</span>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
},
|
||||
size: 130,
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
// Enable Groups column
|
||||
// Group column
|
||||
{
|
||||
accessorKey: 'enable_groups',
|
||||
meta: { label: t('Groups') },
|
||||
header: t('Groups'),
|
||||
meta: { label: t('Group') },
|
||||
header: t('Group'),
|
||||
cell: ({ row }) => {
|
||||
const groups = row.original.enable_groups || []
|
||||
if (groups.length === 0) {
|
||||
@@ -460,8 +134,216 @@ export function usePricingColumns(
|
||||
</TooltipProvider>
|
||||
)
|
||||
},
|
||||
size: 130,
|
||||
size: 120,
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
// Billing Mode column
|
||||
{
|
||||
accessorKey: 'quota_type',
|
||||
meta: { label: t('Billing Mode') },
|
||||
header: t('Billing Mode'),
|
||||
cell: ({ row }) => {
|
||||
const isTokenBased = row.original.quota_type === QUOTA_TYPE_VALUES.TOKEN
|
||||
return (
|
||||
<span className='text-xs font-medium'>
|
||||
{isTokenBased ? t('By Token') : t('By Request')}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
size: 90,
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
// Per-request price column
|
||||
{
|
||||
id: 'per_request_price',
|
||||
meta: { label: t('Per Request') },
|
||||
header: t('Per Request'),
|
||||
cell: ({ row }) => {
|
||||
const model = row.original
|
||||
const isTokenBased = isTokenBasedModel(model)
|
||||
|
||||
if (isTokenBased) {
|
||||
return <span className='text-muted-foreground/30 text-xs'>—</span>
|
||||
}
|
||||
|
||||
const price = stripTrailingZeros(
|
||||
formatRequestPrice(
|
||||
model,
|
||||
showRechargePrice,
|
||||
priceRate,
|
||||
usdExchangeRate
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<span className='font-mono text-sm tabular-nums'>{price}</span>
|
||||
)
|
||||
},
|
||||
size: 90,
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
// Input price column
|
||||
{
|
||||
id: 'input_price',
|
||||
meta: { label: t('Input') },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={`${t('Input')} / ${tokenUnitLabel}`} />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const model = row.original
|
||||
const isTokenBased = isTokenBasedModel(model)
|
||||
|
||||
if (!isTokenBased) {
|
||||
return <span className='text-muted-foreground/30 text-xs'>—</span>
|
||||
}
|
||||
|
||||
const dynamicSummary = getDynamicPricingSummary(model, {
|
||||
tokenUnit,
|
||||
showRechargePrice,
|
||||
priceRate,
|
||||
usdExchangeRate,
|
||||
groupRatioMultiplier: getDynamicDisplayGroupRatio(model),
|
||||
})
|
||||
|
||||
if (dynamicSummary && !dynamicSummary.isSpecialExpression) {
|
||||
const inputEntry = dynamicSummary.entries.find(
|
||||
(entry) => entry.field === 'inputPrice'
|
||||
)
|
||||
if (inputEntry) {
|
||||
return (
|
||||
<span className='font-mono text-sm tabular-nums'>
|
||||
{stripTrailingZeros(inputEntry.formatted)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const inputPrice = stripTrailingZeros(
|
||||
formatPrice(
|
||||
model,
|
||||
'input',
|
||||
tokenUnit,
|
||||
showRechargePrice,
|
||||
priceRate,
|
||||
usdExchangeRate
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<span className='font-mono text-sm tabular-nums'>{inputPrice}</span>
|
||||
)
|
||||
},
|
||||
size: 100,
|
||||
},
|
||||
|
||||
// Cached input price column
|
||||
{
|
||||
id: 'cached_input_price',
|
||||
meta: { label: t('Cached Input') },
|
||||
header: t('Cached Input'),
|
||||
cell: ({ row }) => {
|
||||
const model = row.original
|
||||
const isTokenBased = isTokenBasedModel(model)
|
||||
|
||||
if (!isTokenBased || model.cache_ratio == null) {
|
||||
return <span className='text-muted-foreground/30 text-xs'>—</span>
|
||||
}
|
||||
|
||||
const dynamicSummary = getDynamicPricingSummary(model, {
|
||||
tokenUnit,
|
||||
showRechargePrice,
|
||||
priceRate,
|
||||
usdExchangeRate,
|
||||
groupRatioMultiplier: getDynamicDisplayGroupRatio(model),
|
||||
})
|
||||
|
||||
if (dynamicSummary && !dynamicSummary.isSpecialExpression) {
|
||||
const cacheEntry = dynamicSummary.entries.find(
|
||||
(entry) => entry.field === 'cacheReadPrice'
|
||||
)
|
||||
if (cacheEntry) {
|
||||
return (
|
||||
<span className='font-mono text-sm tabular-nums'>
|
||||
{stripTrailingZeros(cacheEntry.formatted)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const cachedPrice = stripTrailingZeros(
|
||||
formatPrice(
|
||||
model,
|
||||
'cache',
|
||||
tokenUnit,
|
||||
showRechargePrice,
|
||||
priceRate,
|
||||
usdExchangeRate
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<span className='font-mono text-sm tabular-nums'>{cachedPrice}</span>
|
||||
)
|
||||
},
|
||||
size: 100,
|
||||
enableSorting: false,
|
||||
},
|
||||
|
||||
// Output price column
|
||||
{
|
||||
id: 'output_price',
|
||||
meta: { label: t('Output') },
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={`${t('Output')} / ${tokenUnitLabel}`} />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const model = row.original
|
||||
const isTokenBased = isTokenBasedModel(model)
|
||||
|
||||
if (!isTokenBased) {
|
||||
return <span className='text-muted-foreground/30 text-xs'>—</span>
|
||||
}
|
||||
|
||||
const dynamicSummary = getDynamicPricingSummary(model, {
|
||||
tokenUnit,
|
||||
showRechargePrice,
|
||||
priceRate,
|
||||
usdExchangeRate,
|
||||
groupRatioMultiplier: getDynamicDisplayGroupRatio(model),
|
||||
})
|
||||
|
||||
if (dynamicSummary && !dynamicSummary.isSpecialExpression) {
|
||||
const outputEntry = dynamicSummary.entries.find(
|
||||
(entry) => entry.field === 'outputPrice'
|
||||
)
|
||||
if (outputEntry) {
|
||||
return (
|
||||
<span className='font-mono text-sm tabular-nums'>
|
||||
{stripTrailingZeros(outputEntry.formatted)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const outputPrice = stripTrailingZeros(
|
||||
formatPrice(
|
||||
model,
|
||||
'output',
|
||||
tokenUnit,
|
||||
showRechargePrice,
|
||||
priceRate,
|
||||
usdExchangeRate
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<span className='font-mono text-sm tabular-nums'>{outputPrice}</span>
|
||||
)
|
||||
},
|
||||
size: 100,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
+4
-4
@@ -44,10 +44,10 @@ type FilterState = {
|
||||
}
|
||||
|
||||
function normalizeViewMode(value: unknown): ViewMode {
|
||||
if (value === VIEW_MODES.TABLE) {
|
||||
return VIEW_MODES.TABLE
|
||||
if (value === VIEW_MODES.CARD) {
|
||||
return VIEW_MODES.CARD
|
||||
}
|
||||
return VIEW_MODES.CARD
|
||||
return VIEW_MODES.TABLE
|
||||
}
|
||||
|
||||
export function useFilters(models: PricingModel[]) {
|
||||
@@ -129,7 +129,7 @@ export function useFilters(models: PricingModel[]) {
|
||||
)
|
||||
const setViewMode = useCallback(
|
||||
(v: ViewMode) =>
|
||||
updateFilters({ view: v === VIEW_MODES.CARD ? undefined : v }),
|
||||
updateFilters({ view: v === VIEW_MODES.TABLE ? undefined : v }),
|
||||
[updateFilters]
|
||||
)
|
||||
const setShowRechargePrice = useCallback(
|
||||
|
||||
Vendored
+3
@@ -538,6 +538,8 @@
|
||||
"Billing Details": "Billing Details",
|
||||
"Billing History": "Billing History",
|
||||
"Billing Mode": "Billing Mode",
|
||||
"By Token": "By Token",
|
||||
"By Request": "By Request",
|
||||
"Billing Process": "Billing Process",
|
||||
"Billing Source": "Billing Source",
|
||||
"Bind": "Bind",
|
||||
@@ -608,6 +610,7 @@
|
||||
"Cache Write (5m)": "Cache Write (5m)",
|
||||
"Cache write price": "Cache write price",
|
||||
"Cached": "Cached",
|
||||
"Cached Input": "Cached Input",
|
||||
"Cached input": "Cached input",
|
||||
"Calculated price: ${{price}} per 1M tokens": "Calculated price: ${{price}} per 1M tokens",
|
||||
"Calculated ratio: {{ratio}}": "Calculated ratio: {{ratio}}",
|
||||
|
||||
Vendored
+3
@@ -538,6 +538,8 @@
|
||||
"Billing Details": "计费详情",
|
||||
"Billing History": "计费历史",
|
||||
"Billing Mode": "计费模式",
|
||||
"By Token": "按 Token",
|
||||
"By Request": "按次",
|
||||
"Billing Process": "计费过程",
|
||||
"Billing Source": "计费来源",
|
||||
"Bind": "绑定",
|
||||
@@ -608,6 +610,7 @@
|
||||
"Cache Write (5m)": "缓存写入 (5m)",
|
||||
"Cache write price": "缓存写入价格",
|
||||
"Cached": "缓存",
|
||||
"Cached Input": "缓存输入",
|
||||
"Cached input": "缓存输入",
|
||||
"Calculated price: ${{price}} per 1M tokens": "计算价格:${{price}} / 1M tokens",
|
||||
"Calculated ratio: {{ratio}}": "计算倍率:{{ratio}}",
|
||||
|
||||
Reference in New Issue
Block a user