.
For commercial licensing, please contact support@quantumnous.com
*/
-import { useState, useMemo, memo, useCallback, useEffect } from 'react'
+import {
+ useState,
+ useMemo,
+ memo,
+ useCallback,
+ useEffect,
+ forwardRef,
+ useImperativeHandle,
+ useRef,
+} from 'react'
import {
type ColumnDef,
type ColumnFiltersState,
@@ -62,6 +71,7 @@ import {
import { safeJsonParse } from '../utils/json-parser'
import {
ModelPricingEditorPanel,
+ type ModelPricingEditorPanelHandle,
ModelPricingSheet,
type ModelRatioData,
} from './model-pricing-sheet'
@@ -97,6 +107,10 @@ type ModelRow = {
hasConflict: boolean
}
+export type ModelRatioVisualEditorHandle = {
+ commitOpenEditor: () => Promise
+}
+
const STORAGE_KEY = 'model-ratio-column-visibility'
const hasValue = (value?: string) => value !== undefined && value !== ''
@@ -192,8 +206,11 @@ const getPriceDetail = (row: ModelRow, t: (key: string) => string) => {
return details.length > 0 ? details.join(' ยท ') : t('Base input price only')
}
-export const ModelRatioVisualEditor = memo(
- function ModelRatioVisualEditor({
+const ModelRatioVisualEditorComponent = forwardRef<
+ ModelRatioVisualEditorHandle,
+ ModelRatioVisualEditorProps
+>(function ModelRatioVisualEditor(
+ {
modelPrice,
modelRatio,
cacheRatio,
@@ -205,157 +222,140 @@ export const ModelRatioVisualEditor = memo(
billingMode,
billingExpr,
onChange,
- }: ModelRatioVisualEditorProps) {
- const { t } = useTranslation()
- const isMobile = useMediaQuery('(max-width: 767px)')
- const [sheetOpen, setSheetOpen] = useState(false)
- const [editorOpen, setEditorOpen] = useState(false)
- const [editData, setEditData] = useState(null)
- const [sorting, setSorting] = useState([])
- const [columnFilters, setColumnFilters] = useState([])
- const [globalFilter, setGlobalFilter] = useState('')
- const [rowSelection, setRowSelection] = useState({})
- const [pagination, setPagination] = useState({
- pageIndex: 0,
- pageSize: 20,
- })
- const [columnVisibility, setColumnVisibility] = useState(
- () => {
- const saved = localStorage.getItem(STORAGE_KEY)
- if (saved) {
- try {
- return safeJsonParse(saved, {
- fallback: {
- cacheRatio: false,
- createCacheRatio: false,
- imageRatio: false,
- audioRatio: false,
- audioCompletionRatio: false,
- },
- silent: true,
- })
- } catch {
- return {
+ },
+ ref
+) {
+ const { t } = useTranslation()
+ const isMobile = useMediaQuery('(max-width: 767px)')
+ const [sheetOpen, setSheetOpen] = useState(false)
+ const [editorOpen, setEditorOpen] = useState(false)
+ const [editData, setEditData] = useState(null)
+ const [sorting, setSorting] = useState([])
+ const [columnFilters, setColumnFilters] = useState([])
+ const [globalFilter, setGlobalFilter] = useState('')
+ const [rowSelection, setRowSelection] = useState({})
+ const editorPanelRef = useRef(null)
+ const [pagination, setPagination] = useState({
+ pageIndex: 0,
+ pageSize: 20,
+ })
+ const [columnVisibility, setColumnVisibility] = useState(
+ () => {
+ const saved = localStorage.getItem(STORAGE_KEY)
+ if (saved) {
+ try {
+ return safeJsonParse(saved, {
+ fallback: {
cacheRatio: false,
createCacheRatio: false,
imageRatio: false,
audioRatio: false,
audioCompletionRatio: false,
- }
+ },
+ silent: true,
+ })
+ } catch {
+ return {
+ cacheRatio: false,
+ createCacheRatio: false,
+ imageRatio: false,
+ audioRatio: false,
+ audioCompletionRatio: false,
}
}
- return {
- cacheRatio: false,
- createCacheRatio: false,
- imageRatio: false,
- audioRatio: false,
- audioCompletionRatio: false,
- }
}
+ return {
+ cacheRatio: false,
+ createCacheRatio: false,
+ imageRatio: false,
+ audioRatio: false,
+ audioCompletionRatio: false,
+ }
+ }
+ )
+
+ useEffect(() => {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(columnVisibility))
+ }, [columnVisibility])
+
+ const models = useMemo(() => {
+ const priceMap = safeJsonParse>(modelPrice, {
+ fallback: {},
+ context: 'model prices',
+ })
+ const ratioMap = safeJsonParse>(modelRatio, {
+ fallback: {},
+ context: 'model ratios',
+ })
+ const cacheMap = safeJsonParse>(cacheRatio, {
+ fallback: {},
+ context: 'cache ratios',
+ })
+ const createCacheMap = safeJsonParse>(
+ createCacheRatio,
+ { fallback: {}, context: 'create cache ratios' }
)
+ const completionMap = safeJsonParse>(
+ completionRatio,
+ { fallback: {}, context: 'completion ratios' }
+ )
+ const imageMap = safeJsonParse>(imageRatio, {
+ fallback: {},
+ context: 'image ratios',
+ })
+ const audioMap = safeJsonParse>(audioRatio, {
+ fallback: {},
+ context: 'audio ratios',
+ })
+ const audioCompletionMap = safeJsonParse>(
+ audioCompletionRatio,
+ { fallback: {}, context: 'audio completion ratios' }
+ )
+ const billingModeMap = safeJsonParse>(billingMode, {
+ fallback: {},
+ context: 'billing mode',
+ })
+ const billingExprMap = safeJsonParse>(billingExpr, {
+ fallback: {},
+ context: 'billing expression',
+ })
- useEffect(() => {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(columnVisibility))
- }, [columnVisibility])
+ const modelNames = new Set([
+ ...Object.keys(priceMap),
+ ...Object.keys(ratioMap),
+ ...Object.keys(cacheMap),
+ ...Object.keys(createCacheMap),
+ ...Object.keys(completionMap),
+ ...Object.keys(imageMap),
+ ...Object.keys(audioMap),
+ ...Object.keys(audioCompletionMap),
+ ...Object.keys(billingModeMap),
+ ...Object.keys(billingExprMap),
+ ])
- const models = useMemo(() => {
- const priceMap = safeJsonParse>(modelPrice, {
- fallback: {},
- context: 'model prices',
- })
- const ratioMap = safeJsonParse>(modelRatio, {
- fallback: {},
- context: 'model ratios',
- })
- const cacheMap = safeJsonParse>(cacheRatio, {
- fallback: {},
- context: 'cache ratios',
- })
- const createCacheMap = safeJsonParse>(
- createCacheRatio,
- { fallback: {}, context: 'create cache ratios' }
- )
- const completionMap = safeJsonParse>(
- completionRatio,
- { fallback: {}, context: 'completion ratios' }
- )
- const imageMap = safeJsonParse>(imageRatio, {
- fallback: {},
- context: 'image ratios',
- })
- const audioMap = safeJsonParse>(audioRatio, {
- fallback: {},
- context: 'audio ratios',
- })
- const audioCompletionMap = safeJsonParse>(
- audioCompletionRatio,
- { fallback: {}, context: 'audio completion ratios' }
- )
- const billingModeMap = safeJsonParse>(
- billingMode,
- {
- fallback: {},
- context: 'billing mode',
- }
- )
- const billingExprMap = safeJsonParse>(
- billingExpr,
- {
- fallback: {},
- context: 'billing expression',
- }
- )
-
- const modelNames = new Set([
- ...Object.keys(priceMap),
- ...Object.keys(ratioMap),
- ...Object.keys(cacheMap),
- ...Object.keys(createCacheMap),
- ...Object.keys(completionMap),
- ...Object.keys(imageMap),
- ...Object.keys(audioMap),
- ...Object.keys(audioCompletionMap),
- ...Object.keys(billingModeMap),
- ...Object.keys(billingExprMap),
- ])
-
- const modelData: ModelRow[] = Array.from(modelNames).map((name) => {
- const price = priceMap[name]?.toString() || ''
- const ratio = ratioMap[name]?.toString() || ''
- const cache = cacheMap[name]?.toString() || ''
- const createCache = createCacheMap[name]?.toString() || ''
- const completion = completionMap[name]?.toString() || ''
- const image = imageMap[name]?.toString() || ''
- const audio = audioMap[name]?.toString() || ''
- const audioCompletion = audioCompletionMap[name]?.toString() || ''
-
- const modeForModel = billingModeMap[name]
- if (modeForModel === 'tiered_expr') {
- // Tiered_expr models may also retain ratio/price values as fallback
- // during multi-instance sync delays. We preserve them in the row so
- // the edit dialog round-trip and the next save don't drop them.
- const fullExpr = billingExprMap[name] || ''
- const { billingExpr: pureExpr, requestRuleExpr } =
- splitBillingExprAndRequestRules(fullExpr)
- return {
- name,
- billingMode: 'tiered_expr',
- billingExpr: pureExpr,
- requestRuleExpr,
- price,
- ratio,
- cacheRatio: cache,
- createCacheRatio: createCache,
- completionRatio: completion,
- imageRatio: image,
- audioRatio: audio,
- audioCompletionRatio: audioCompletion,
- hasConflict: false,
- }
- }
+ const modelData: ModelRow[] = Array.from(modelNames).map((name) => {
+ const price = priceMap[name]?.toString() || ''
+ const ratio = ratioMap[name]?.toString() || ''
+ const cache = cacheMap[name]?.toString() || ''
+ const createCache = createCacheMap[name]?.toString() || ''
+ const completion = completionMap[name]?.toString() || ''
+ const image = imageMap[name]?.toString() || ''
+ const audio = audioMap[name]?.toString() || ''
+ const audioCompletion = audioCompletionMap[name]?.toString() || ''
+ const modeForModel = billingModeMap[name]
+ if (modeForModel === 'tiered_expr') {
+ // Tiered_expr models may also retain ratio/price values as fallback
+ // during multi-instance sync delays. We preserve them in the row so
+ // the edit dialog round-trip and the next save don't drop them.
+ const fullExpr = billingExprMap[name] || ''
+ const { billingExpr: pureExpr, requestRuleExpr } =
+ splitBillingExprAndRequestRules(fullExpr)
return {
name,
+ billingMode: 'tiered_expr',
+ billingExpr: pureExpr,
+ requestRuleExpr,
price,
ratio,
cacheRatio: cache,
@@ -364,21 +364,197 @@ export const ModelRatioVisualEditor = memo(
imageRatio: image,
audioRatio: audio,
audioCompletionRatio: audioCompletion,
- billingMode: price !== '' ? 'per-request' : 'per-token',
- hasConflict:
- price !== '' &&
- (ratio !== '' ||
- completion !== '' ||
- cache !== '' ||
- createCache !== '' ||
- image !== '' ||
- audio !== '' ||
- audioCompletion !== ''),
+ hasConflict: false,
}
- })
+ }
- return modelData.sort((a, b) => a.name.localeCompare(b.name))
- }, [
+ return {
+ name,
+ price,
+ ratio,
+ cacheRatio: cache,
+ createCacheRatio: createCache,
+ completionRatio: completion,
+ imageRatio: image,
+ audioRatio: audio,
+ audioCompletionRatio: audioCompletion,
+ billingMode: price !== '' ? 'per-request' : 'per-token',
+ hasConflict:
+ price !== '' &&
+ (ratio !== '' ||
+ completion !== '' ||
+ cache !== '' ||
+ createCache !== '' ||
+ image !== '' ||
+ audio !== '' ||
+ audioCompletion !== ''),
+ }
+ })
+
+ return modelData.sort((a, b) => a.name.localeCompare(b.name))
+ }, [
+ modelPrice,
+ modelRatio,
+ cacheRatio,
+ createCacheRatio,
+ completionRatio,
+ imageRatio,
+ audioRatio,
+ audioCompletionRatio,
+ billingMode,
+ billingExpr,
+ ])
+
+ 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(() => {
+ setEditData(null)
+ setEditorOpen(true)
+ if (isMobile) setSheetOpen(true)
+ }, [isMobile])
+
+ const handleCancel = useCallback(() => {
+ setEditData(null)
+ setEditorOpen(false)
+ setSheetOpen(false)
+ }, [])
+
+ const handleGlobalFilterChange = useCallback>(
+ (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(
+ (name: string) => {
+ const priceMap = safeJsonParse>(modelPrice, {
+ fallback: {},
+ silent: true,
+ })
+ const ratioMap = safeJsonParse>(modelRatio, {
+ fallback: {},
+ silent: true,
+ })
+ const cacheMap = safeJsonParse>(cacheRatio, {
+ fallback: {},
+ silent: true,
+ })
+ const createCacheMap = safeJsonParse>(
+ createCacheRatio,
+ { fallback: {}, silent: true }
+ )
+ const completionMap = safeJsonParse>(
+ completionRatio,
+ { fallback: {}, silent: true }
+ )
+ const imageMap = safeJsonParse>(imageRatio, {
+ fallback: {},
+ silent: true,
+ })
+ const audioMap = safeJsonParse>(audioRatio, {
+ fallback: {},
+ silent: true,
+ })
+ const audioCompletionMap = safeJsonParse>(
+ audioCompletionRatio,
+ { fallback: {}, silent: true }
+ )
+ const billingModeMap = safeJsonParse>(
+ billingMode,
+ { fallback: {}, silent: true }
+ )
+ const billingExprMap = safeJsonParse>(
+ billingExpr,
+ { fallback: {}, silent: true }
+ )
+
+ 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]
+
+ onChange('ModelPrice', JSON.stringify(priceMap, null, 2))
+ onChange('ModelRatio', JSON.stringify(ratioMap, null, 2))
+ onChange('CacheRatio', JSON.stringify(cacheMap, null, 2))
+ onChange('CreateCacheRatio', JSON.stringify(createCacheMap, null, 2))
+ onChange('CompletionRatio', JSON.stringify(completionMap, null, 2))
+ onChange('ImageRatio', JSON.stringify(imageMap, null, 2))
+ onChange('AudioRatio', JSON.stringify(audioMap, null, 2))
+ onChange(
+ 'AudioCompletionRatio',
+ JSON.stringify(audioCompletionMap, null, 2)
+ )
+ onChange(
+ 'billing_setting.billing_mode',
+ JSON.stringify(billingModeMap, null, 2)
+ )
+ onChange(
+ 'billing_setting.billing_expr',
+ JSON.stringify(billingExprMap, null, 2)
+ )
+ },
+ [
modelPrice,
modelRatio,
cacheRatio,
@@ -389,127 +565,210 @@ export const ModelRatioVisualEditor = memo(
audioCompletionRatio,
billingMode,
billingExpr,
- ])
+ onChange,
+ ]
+ )
- 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>
+ const columns = useMemo[]>(() => {
+ return [
+ {
+ id: 'select',
+ header: ({ table }) => (
+
+ table.toggleAllPageRowsSelected(!!value)
+ }
+ aria-label={t('Select all')}
+ className='translate-y-[2px]'
+ />
),
- [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)
+ cell: ({ row }) => (
+ row.toggleSelected(!!value)}
+ aria-label={t('Select row')}
+ className='translate-y-[2px]'
+ />
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ meta: { label: t('Select') },
},
- [isMobile]
- )
-
- const handleAdd = useCallback(() => {
- setEditData(null)
- setEditorOpen(true)
- if (isMobile) setSheetOpen(true)
- }, [isMobile])
-
- const handleCancel = useCallback(() => {
- setEditData(null)
- setEditorOpen(false)
- setSheetOpen(false)
- }, [])
-
- const handleGlobalFilterChange = useCallback>(
- (updater) => {
- setGlobalFilter((previous) => {
- const next =
- typeof updater === 'function' ? updater(previous) : updater
- if (next !== previous) {
- setEditData(null)
- setEditorOpen(false)
- setSheetOpen(false)
- }
- return next
- })
+ {
+ accessorKey: 'name',
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => (
+
+ {row.getValue('name')}
+ {row.original.billingMode === 'tiered_expr' && (
+
+ )}
+ {row.original.hasConflict && (
+
+ )}
+
+ ),
+ enableHiding: false,
},
- []
- )
+ {
+ accessorKey: 'billingMode',
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => (
+
+ ),
+ filterFn: (row, id, value) =>
+ filterBySelectedValues(row.getValue(id), value),
+ meta: { label: t('Mode') },
+ },
+ {
+ id: 'priceSummary',
+ header: ({ column }) => (
+
+ ),
+ cell: ({ row }) => (
+
+
+ {getPriceSummary(row.original, t)}
+
+
+ {getPriceDetail(row.original, t)}
+
+
+ ),
+ sortingFn: (rowA, rowB) =>
+ getPriceSummary(rowA.original, t).localeCompare(
+ getPriceSummary(rowB.original, t)
+ ),
+ meta: { label: t('Price summary') },
+ },
+ {
+ id: 'actions',
+ cell: ({ row }) => (
+
+
+
+
+ ),
+ enableHiding: false,
+ },
+ ]
+ }, [handleEdit, handleDelete, t])
- const handleDelete = useCallback(
- (name: string) => {
- const priceMap = safeJsonParse>(modelPrice, {
- fallback: {},
- silent: true,
- })
- const ratioMap = safeJsonParse>(modelRatio, {
- fallback: {},
- silent: true,
- })
- const cacheMap = safeJsonParse>(cacheRatio, {
- fallback: {},
- silent: true,
- })
- const createCacheMap = safeJsonParse>(
- createCacheRatio,
- { fallback: {}, silent: true }
- )
- const completionMap = safeJsonParse>(
- completionRatio,
- { fallback: {}, silent: true }
- )
- const imageMap = safeJsonParse>(imageRatio, {
- fallback: {},
- silent: true,
- })
- const audioMap = safeJsonParse>(audioRatio, {
- fallback: {},
- silent: true,
- })
- const audioCompletionMap = safeJsonParse>(
- audioCompletionRatio,
- { fallback: {}, silent: true }
- )
- const billingModeMap = safeJsonParse>(
- billingMode,
- { fallback: {}, silent: true }
- )
- const billingExprMap = safeJsonParse>(
- billingExpr,
- { fallback: {}, silent: true }
- )
+ const table = useReactTable({
+ data: models,
+ columns,
+ state: {
+ sorting,
+ columnFilters,
+ globalFilter,
+ columnVisibility,
+ pagination,
+ rowSelection,
+ },
+ enableRowSelection: true,
+ onSortingChange: setSorting,
+ onColumnFiltersChange: setColumnFilters,
+ onGlobalFilterChange: handleGlobalFilterChange,
+ onColumnVisibilityChange: setColumnVisibility,
+ onPaginationChange: setPagination,
+ onRowSelectionChange: setRowSelection,
+ autoResetPageIndex: false,
+ getCoreRowModel: getCoreRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getFacetedRowModel: getFacetedRowModel(),
+ getFacetedUniqueValues: getFacetedUniqueValues(),
+ globalFilterFn: (row, _columnId, filterValue) => {
+ const searchValue = String(filterValue).toLowerCase()
+ return row.original.name.toLowerCase().includes(searchValue)
+ },
+ })
+ const persistPricingData = useCallback(
+ (data: ModelRatioData, targetNames: string[] = [data.name]) => {
+ const priceMap = safeJsonParse>(modelPrice, {
+ fallback: {},
+ silent: true,
+ })
+ const ratioMap = safeJsonParse>(modelRatio, {
+ fallback: {},
+ silent: true,
+ })
+ const cacheMap = safeJsonParse>(cacheRatio, {
+ fallback: {},
+ silent: true,
+ })
+ const createCacheMap = safeJsonParse>(
+ createCacheRatio,
+ { fallback: {}, silent: true }
+ )
+ const completionMap = safeJsonParse>(
+ completionRatio,
+ { fallback: {}, silent: true }
+ )
+ const imageMap = safeJsonParse>(imageRatio, {
+ fallback: {},
+ silent: true,
+ })
+ const audioMap = safeJsonParse>(audioRatio, {
+ fallback: {},
+ silent: true,
+ })
+ const audioCompletionMap = safeJsonParse>(
+ audioCompletionRatio,
+ { fallback: {}, silent: true }
+ )
+ const billingModeMap = safeJsonParse>(
+ billingMode,
+ { fallback: {}, silent: true }
+ )
+ const billingExprMap = safeJsonParse>(
+ billingExpr,
+ { fallback: {}, silent: true }
+ )
+
+ const setIfPresent = (
+ target: Record,
+ name: string,
+ value: string | undefined
+ ) => {
+ if (!value || value === '') return
+ const parsed = parseFloat(value)
+ if (Number.isFinite(parsed)) target[name] = parsed
+ }
+
+ targetNames.forEach((name) => {
delete priceMap[name]
delete ratioMap[name]
delete cacheMap[name]
@@ -521,514 +780,283 @@ export const ModelRatioVisualEditor = memo(
delete billingModeMap[name]
delete billingExprMap[name]
- onChange('ModelPrice', JSON.stringify(priceMap, null, 2))
- onChange('ModelRatio', JSON.stringify(ratioMap, null, 2))
- onChange('CacheRatio', JSON.stringify(cacheMap, null, 2))
- onChange('CreateCacheRatio', JSON.stringify(createCacheMap, null, 2))
- onChange('CompletionRatio', JSON.stringify(completionMap, null, 2))
- onChange('ImageRatio', JSON.stringify(imageMap, null, 2))
- onChange('AudioRatio', JSON.stringify(audioMap, null, 2))
- onChange(
- 'AudioCompletionRatio',
- JSON.stringify(audioCompletionMap, null, 2)
- )
- onChange(
- 'billing_setting.billing_mode',
- JSON.stringify(billingModeMap, null, 2)
- )
- onChange(
- 'billing_setting.billing_expr',
- JSON.stringify(billingExprMap, null, 2)
- )
- },
- [
- modelPrice,
- modelRatio,
- cacheRatio,
- createCacheRatio,
- completionRatio,
- imageRatio,
- audioRatio,
- audioCompletionRatio,
- billingMode,
- billingExpr,
- onChange,
- ]
- )
-
- const columns = useMemo[]>(() => {
- return [
- {
- id: 'select',
- header: ({ table }) => (
-
- table.toggleAllPageRowsSelected(!!value)
- }
- aria-label={t('Select all')}
- className='translate-y-[2px]'
- />
- ),
- cell: ({ row }) => (
- row.toggleSelected(!!value)}
- aria-label={t('Select row')}
- className='translate-y-[2px]'
- />
- ),
- enableSorting: false,
- enableHiding: false,
- meta: { label: t('Select') },
- },
- {
- accessorKey: 'name',
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => (
-
- {row.getValue('name')}
- {row.original.billingMode === 'tiered_expr' && (
-
- )}
- {row.original.hasConflict && (
-
- )}
-
- ),
- enableHiding: false,
- },
- {
- accessorKey: 'billingMode',
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => (
-
- ),
- filterFn: (row, id, value) =>
- filterBySelectedValues(row.getValue(id), value),
- meta: { label: t('Mode') },
- },
- {
- id: 'priceSummary',
- header: ({ column }) => (
-
- ),
- cell: ({ row }) => (
-
-
- {getPriceSummary(row.original, t)}
-
-
- {getPriceDetail(row.original, t)}
-
-
- ),
- sortingFn: (rowA, rowB) =>
- getPriceSummary(rowA.original, t).localeCompare(
- getPriceSummary(rowB.original, t)
- ),
- meta: { label: t('Price summary') },
- },
- {
- id: 'actions',
- cell: ({ row }) => (
-
-
-
-
- ),
- enableHiding: false,
- },
- ]
- }, [handleEdit, handleDelete, t])
-
- const table = useReactTable({
- data: models,
- columns,
- state: {
- sorting,
- columnFilters,
- globalFilter,
- columnVisibility,
- pagination,
- rowSelection,
- },
- enableRowSelection: true,
- onSortingChange: setSorting,
- onColumnFiltersChange: setColumnFilters,
- onGlobalFilterChange: handleGlobalFilterChange,
- onColumnVisibilityChange: setColumnVisibility,
- onPaginationChange: setPagination,
- onRowSelectionChange: setRowSelection,
- autoResetPageIndex: false,
- getCoreRowModel: getCoreRowModel(),
- getFilteredRowModel: getFilteredRowModel(),
- getSortedRowModel: getSortedRowModel(),
- getPaginationRowModel: getPaginationRowModel(),
- getFacetedRowModel: getFacetedRowModel(),
- getFacetedUniqueValues: getFacetedUniqueValues(),
- globalFilterFn: (row, _columnId, filterValue) => {
- const searchValue = String(filterValue).toLowerCase()
- return row.original.name.toLowerCase().includes(searchValue)
- },
- })
-
- const persistPricingData = useCallback(
- (data: ModelRatioData, targetNames: string[] = [data.name]) => {
- const priceMap = safeJsonParse>(modelPrice, {
- fallback: {},
- silent: true,
- })
- const ratioMap = safeJsonParse>(modelRatio, {
- fallback: {},
- silent: true,
- })
- const cacheMap = safeJsonParse>(cacheRatio, {
- fallback: {},
- silent: true,
- })
- const createCacheMap = safeJsonParse>(
- createCacheRatio,
- { fallback: {}, silent: true }
- )
- const completionMap = safeJsonParse>(
- completionRatio,
- { fallback: {}, silent: true }
- )
- const imageMap = safeJsonParse>(imageRatio, {
- fallback: {},
- silent: true,
- })
- const audioMap = safeJsonParse>(audioRatio, {
- fallback: {},
- silent: true,
- })
- const audioCompletionMap = safeJsonParse>(
- audioCompletionRatio,
- { fallback: {}, silent: true }
- )
- const billingModeMap = safeJsonParse>(
- billingMode,
- { fallback: {}, silent: true }
- )
- const billingExprMap = safeJsonParse>(
- billingExpr,
- { fallback: {}, silent: true }
- )
-
- const setIfPresent = (
- target: Record,
- name: string,
- value: string | undefined
- ) => {
- if (!value || value === '') return
- const parsed = parseFloat(value)
- if (Number.isFinite(parsed)) target[name] = parsed
- }
-
- 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)
+ 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)
+ }
+ })
- onChange('ModelPrice', JSON.stringify(priceMap, null, 2))
- onChange('ModelRatio', JSON.stringify(ratioMap, null, 2))
- onChange('CacheRatio', JSON.stringify(cacheMap, null, 2))
- onChange('CreateCacheRatio', JSON.stringify(createCacheMap, null, 2))
- onChange('CompletionRatio', JSON.stringify(completionMap, null, 2))
- onChange('ImageRatio', JSON.stringify(imageMap, null, 2))
- onChange('AudioRatio', JSON.stringify(audioMap, null, 2))
- onChange(
- 'AudioCompletionRatio',
- JSON.stringify(audioCompletionMap, null, 2)
- )
- onChange(
- 'billing_setting.billing_mode',
- JSON.stringify(billingModeMap, null, 2)
- )
- onChange(
- 'billing_setting.billing_expr',
- JSON.stringify(billingExprMap, null, 2)
- )
- },
- [
- modelPrice,
- modelRatio,
- cacheRatio,
- createCacheRatio,
- completionRatio,
- imageRatio,
- audioRatio,
- audioCompletionRatio,
- billingMode,
- billingExpr,
- onChange,
- ]
+ onChange('ModelPrice', JSON.stringify(priceMap, null, 2))
+ onChange('ModelRatio', JSON.stringify(ratioMap, null, 2))
+ onChange('CacheRatio', JSON.stringify(cacheMap, null, 2))
+ onChange('CreateCacheRatio', JSON.stringify(createCacheMap, null, 2))
+ onChange('CompletionRatio', JSON.stringify(completionMap, null, 2))
+ onChange('ImageRatio', JSON.stringify(imageMap, null, 2))
+ onChange('AudioRatio', JSON.stringify(audioMap, null, 2))
+ onChange(
+ 'AudioCompletionRatio',
+ JSON.stringify(audioCompletionMap, null, 2)
+ )
+ onChange(
+ 'billing_setting.billing_mode',
+ JSON.stringify(billingModeMap, null, 2)
+ )
+ onChange(
+ 'billing_setting.billing_expr',
+ JSON.stringify(billingExprMap, null, 2)
+ )
+ },
+ [
+ modelPrice,
+ modelRatio,
+ cacheRatio,
+ createCacheRatio,
+ completionRatio,
+ imageRatio,
+ audioRatio,
+ audioCompletionRatio,
+ billingMode,
+ billingExpr,
+ onChange,
+ ]
+ )
+
+ const handleSave = useCallback(
+ (data: ModelRatioData) => {
+ persistPricingData(data)
+ setEditData(data)
+ setEditorOpen(true)
+ toast.success(
+ t('Pricing changes saved to draft. Click "Save model prices" to apply.')
+ )
+ },
+ [persistPricingData, t]
+ )
+
+ 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 handleSave = useCallback(
- (data: ModelRatioData) => {
+ useImperativeHandle(
+ ref,
+ () => ({
+ commitOpenEditor: async () => {
+ if (!editorOpen || !editorPanelRef.current) return true
+ const data = await editorPanelRef.current.commitDraft()
+ if (!data) return false
persistPricingData(data)
setEditData(data)
- setEditorOpen(true)
- toast.success(
- t(
- 'Pricing changes saved to draft. Click "Save model prices" to apply.'
- )
- )
+ return true
},
- [persistPricingData, t]
- )
+ }),
+ [editorOpen, persistPricingData]
+ )
- const handleBatchCopy = useCallback(() => {
- if (!editData) {
- toast.error(t('Open a source model first'))
- return
- }
+ const selectedTargetCount = table.getFilteredSelectedRowModel().rows.length
- const targetNames = table
- .getFilteredSelectedRowModel()
- .rows.map((row) => row.original.name)
+ return (
+
+
+
+
+
+ {t('Add model')}
+
+ }
+ />
- if (targetNames.length === 0) {
- toast.error(t('Select at least one target model'))
- return
- }
+ {table.getRowModel().rows.length === 0 ? (
+
+ {table.getState().globalFilter
+ ? t('No models match your search')
+ : t('No models configured. Use Add model to get started.')}
+
+ ) : (
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+
+ ))}
+
+ ))}
+
+
+ {table.getRowModel().rows.map((row) => (
+ {
+ const target = event.target as HTMLElement
+ if (target.closest('button, [role="checkbox"]')) return
+ handleEdit(row.original)
+ }}
+ >
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+
+ ))}
+
+ ))}
+
+
+
+ )}
- 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 (
-
-
-
-
-
- {t('Add model')}
-
- }
- />
-
- {table.getRowModel().rows.length === 0 ? (
-
- {table.getState().globalFilter
- ? t('No models match your search')
- : t('No models configured. Use Add model to get started.')}
-
- ) : (
-
-
-
- {table.getHeaderGroups().map((headerGroup) => (
-
- {headerGroup.headers.map((header) => (
-
- {header.isPlaceholder
- ? null
- : flexRender(
- header.column.columnDef.header,
- header.getContext()
- )}
-
- ))}
-
- ))}
-
-
- {table.getRowModel().rows.map((row) => (
- {
- const target = event.target as HTMLElement
- if (target.closest('button, [role="checkbox"]'))
- return
- handleEdit(row.original)
- }}
- >
- {row.getVisibleCells().map((cell) => (
-
- {flexRender(
- cell.column.columnDef.cell,
- cell.getContext()
- )}
-
- ))}
-
- ))}
-
-
-
- )}
-
- {table.getRowModel().rows.length > 0 && (
-
- )}
-
-
-
- {editorOpen ? (
-
- ) : (
-
-
- {t('Select a model to edit pricing')}
-
-
- {t(
- 'Use the full-width table to scan prices, then select a row to edit it here.'
- )}
-
-
-
- )}
-
+ {table.getRowModel().rows.length > 0 && (
+
+ )}
-
-
-
-
- {isMobile && (
-
- )}
+
+ {editorOpen ? (
+
+ ) : (
+
+
+ {t('Select a model to edit pricing')}
+
+
+ {t(
+ 'Use the full-width table to scan prices, then select a row to edit it here.'
+ )}
+
+
+
+ )}
+
- )
- },
+
+
+
+
+
+ {isMobile && (
+
+ )}
+
+ )
+})
+
+export const ModelRatioVisualEditor = memo(
+ ModelRatioVisualEditorComponent,
// Custom equality check - only re-render if JSON props actually changed
(prevProps, nextProps) => {
return (