diff --git a/web/default/src/features/system-settings/models/model-ratio-form.tsx b/web/default/src/features/system-settings/models/model-ratio-form.tsx
index 640d6090..7dbd7697 100644
--- a/web/default/src/features/system-settings/models/model-ratio-form.tsx
+++ b/web/default/src/features/system-settings/models/model-ratio-form.tsx
@@ -18,9 +18,10 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { useQuery } from '@tanstack/react-query'
import { Code2, Eye, RotateCcw, Save } from 'lucide-react'
-import { memo, useCallback, useRef, useState } from 'react'
-import { type UseFormReturn } from 'react-hook-form'
+import { memo, useCallback, useEffect, useRef, useState } from 'react'
+import type { UseFormReturn } from 'react-hook-form'
import { useTranslation } from 'react-i18next'
+import { toast } from 'sonner'
import { JsonCodeEditor } from '@/components/json-code-editor'
import { Button } from '@/components/ui/button'
@@ -180,6 +181,18 @@ export const ModelRatioForm = memo(function ModelRatioForm({
enabled: isUnsetVariant,
})
+ const enabledModelsError = isUnsetVariant
+ ? enabledModelsQuery.isError ||
+ (enabledModelsQuery.data !== undefined &&
+ !enabledModelsQuery.data.success)
+ : false
+ const enabledModelsErrorMessage = enabledModelsQuery.data?.message
+
+ useEffect(() => {
+ if (!enabledModelsError) return
+ toast.error(enabledModelsErrorMessage || t('Failed to load enabled models'))
+ }, [enabledModelsError, enabledModelsErrorMessage, t])
+
const handleFieldChange = useCallback(
(field: keyof ModelFormValues, value: string) => {
form.setValue(field, value, {
@@ -272,6 +285,9 @@ export const ModelRatioForm = memo(function ModelRatioForm({
candidateModelNames={
isUnsetVariant ? enabledModelsQuery.data?.data : undefined
}
+ candidateModelsLoading={
+ isUnsetVariant && enabledModelsQuery.isLoading
+ }
filterMode={isUnsetVariant ? 'unset' : 'all'}
onSave={handleSave}
isSaving={isSaving}
diff --git a/web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx b/web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
index 3606d2aa..827ef2eb 100644
--- a/web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
+++ b/web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
@@ -16,13 +16,13 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import {
- type ColumnFiltersState,
- type OnChangeFn,
- type PaginationState,
- type RowSelectionState,
- type VisibilityState,
- type SortingState,
+import type {
+ ColumnFiltersState,
+ OnChangeFn,
+ PaginationState,
+ RowSelectionState,
+ VisibilityState,
+ SortingState,
} from '@tanstack/react-table'
import { Copy, Plus } from 'lucide-react'
import {
@@ -51,6 +51,7 @@ import { combineBillingExpr } from '@/features/pricing/lib/billing-expr'
import { useMediaQuery } from '@/hooks'
import { safeJsonParse } from '../utils/json-parser'
+import type { PricingMode } from './model-pricing-core'
import {
ModelPricingEditorPanel,
type ModelPricingEditorPanelHandle,
@@ -87,6 +88,7 @@ type ModelRatioVisualEditorProps = {
billingMode: string
billingExpr: string
candidateModelNames?: string[]
+ candidateModelsLoading?: boolean
filterMode?: 'all' | 'unset'
onChange: (field: string, value: string) => void
onSave: () => void | Promise
@@ -125,6 +127,7 @@ const ModelRatioVisualEditorComponent = forwardRef<
billingMode,
billingExpr,
candidateModelNames,
+ candidateModelsLoading,
filterMode = 'all',
onChange,
onSave,
@@ -219,7 +222,7 @@ const ModelRatioVisualEditorComponent = forwardRef<
...draftByName.keys(),
])
- return Array.from(modelNames)
+ return [...modelNames]
.map((name) => {
const saved = savedByName.get(name)
const draft = draftByName.get(name)
@@ -289,6 +292,12 @@ const ModelRatioVisualEditorComponent = forwardRef<
const handleEdit = useCallback(
(model: ModelRow) => {
const editableModel = model.draft ?? model.saved ?? model
+ let editBillingMode: PricingMode = 'per-token'
+ if (editableModel.billingMode === 'tiered_expr') {
+ editBillingMode = 'tiered_expr'
+ } else if (editableModel.price && editableModel.price !== '') {
+ editBillingMode = 'per-request'
+ }
setEditData({
name: editableModel.name,
price: editableModel.price,
@@ -299,12 +308,7 @@ const ModelRatioVisualEditorComponent = forwardRef<
imageRatio: editableModel.imageRatio,
audioRatio: editableModel.audioRatio,
audioCompletionRatio: editableModel.audioCompletionRatio,
- billingMode:
- editableModel.billingMode === 'tiered_expr'
- ? 'tiered_expr'
- : editableModel.price && editableModel.price !== ''
- ? 'per-request'
- : 'per-token',
+ billingMode: editBillingMode,
billingExpr: editableModel.billingExpr,
requestRuleExpr: editableModel.requestRuleExpr,
})
@@ -632,7 +636,11 @@ const ModelRatioVisualEditorComponent = forwardRef<
return
}
- persistPricingData(sourceData, targetNames)
+ // Persist to the source model too, so targets never carry pricing the
+ // source itself would lose if the editor draft were abandoned.
+ persistPricingData(sourceData, [
+ ...new Set([sourceData.name, ...targetNames]),
+ ])
table.resetRowSelection()
toast.success(
t('Applied {{name}} pricing to {{count}} models', {
@@ -663,7 +671,9 @@ const ModelRatioVisualEditorComponent = forwardRef<
if (table.getState().globalFilter) {
emptyStateText = t('No models match your search')
} else if (filterMode === 'unset') {
- emptyStateText = t('No models with unset prices')
+ emptyStateText = candidateModelsLoading
+ ? t('Loading...')
+ : t('No models with unset prices')
}
return (
@@ -821,6 +831,17 @@ export const ModelRatioVisualEditor = memo(
// Custom equality check - only re-render if JSON props actually changed
(prevProps, nextProps) => {
return (
+ prevProps.savedModelPrice === nextProps.savedModelPrice &&
+ prevProps.savedModelRatio === nextProps.savedModelRatio &&
+ prevProps.savedCacheRatio === nextProps.savedCacheRatio &&
+ prevProps.savedCreateCacheRatio === nextProps.savedCreateCacheRatio &&
+ prevProps.savedCompletionRatio === nextProps.savedCompletionRatio &&
+ prevProps.savedImageRatio === nextProps.savedImageRatio &&
+ prevProps.savedAudioRatio === nextProps.savedAudioRatio &&
+ prevProps.savedAudioCompletionRatio ===
+ nextProps.savedAudioCompletionRatio &&
+ prevProps.savedBillingMode === nextProps.savedBillingMode &&
+ prevProps.savedBillingExpr === nextProps.savedBillingExpr &&
prevProps.modelPrice === nextProps.modelPrice &&
prevProps.modelRatio === nextProps.modelRatio &&
prevProps.cacheRatio === nextProps.cacheRatio &&
@@ -832,6 +853,7 @@ export const ModelRatioVisualEditor = memo(
prevProps.billingMode === nextProps.billingMode &&
prevProps.billingExpr === nextProps.billingExpr &&
prevProps.candidateModelNames === nextProps.candidateModelNames &&
+ prevProps.candidateModelsLoading === nextProps.candidateModelsLoading &&
prevProps.filterMode === nextProps.filterMode &&
prevProps.onChange === nextProps.onChange &&
prevProps.onSave === nextProps.onSave &&
diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json
index a9f5cec0..9eba821e 100644
--- a/web/default/src/i18n/locales/en.json
+++ b/web/default/src/i18n/locales/en.json
@@ -1845,6 +1845,7 @@
"Failed to load": "Failed to load",
"Failed to load API keys": "Failed to load API keys",
"Failed to load billing history": "Failed to load billing history",
+ "Failed to load enabled models": "Failed to load enabled models",
"Failed to load home page content": "Failed to load home page content",
"Failed to load image": "Failed to load image",
"Failed to load key status": "Failed to load key status",
diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json
index 72157673..f4014a48 100644
--- a/web/default/src/i18n/locales/fr.json
+++ b/web/default/src/i18n/locales/fr.json
@@ -1845,6 +1845,7 @@
"Failed to load": "Échec du chargement",
"Failed to load API keys": "Échec du chargement des Clés API",
"Failed to load billing history": "Échec du chargement de l'historique de facturation",
+ "Failed to load enabled models": "Échec du chargement des modèles activés",
"Failed to load home page content": "Échec du chargement du contenu de la page d'accueil",
"Failed to load image": "Échec du chargement de l'image",
"Failed to load key status": "Échec du chargement du statut des clés",
diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json
index 9845303e..527f49b9 100644
--- a/web/default/src/i18n/locales/ja.json
+++ b/web/default/src/i18n/locales/ja.json
@@ -1845,6 +1845,7 @@
"Failed to load": "読み込みに失敗しました",
"Failed to load API keys": "APIキーの読み込みに失敗しました",
"Failed to load billing history": "請求履歴の読み込みに失敗しました",
+ "Failed to load enabled models": "有効なモデルの取得に失敗しました",
"Failed to load home page content": "ホームページの内容の読み込みに失敗しました",
"Failed to load image": "画像の読み込みに失敗しました",
"Failed to load key status": "キー状態の読み込みに失敗しました",
diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json
index 9765f086..0c424ad5 100644
--- a/web/default/src/i18n/locales/ru.json
+++ b/web/default/src/i18n/locales/ru.json
@@ -1845,6 +1845,7 @@
"Failed to load": "Не удалось загрузить",
"Failed to load API keys": "Не удалось загрузить API ключи",
"Failed to load billing history": "Не удалось загрузить историю платежей",
+ "Failed to load enabled models": "Не удалось загрузить включённые модели",
"Failed to load home page content": "Не удалось загрузить содержимое главной страницы",
"Failed to load image": "Не удалось загрузить изображение",
"Failed to load key status": "Не удалось загрузить статус ключей",
diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json
index bcef090c..17fc695a 100644
--- a/web/default/src/i18n/locales/vi.json
+++ b/web/default/src/i18n/locales/vi.json
@@ -1845,6 +1845,7 @@
"Failed to load": "Tải thất bại",
"Failed to load API keys": "Không thể tải khóa API",
"Failed to load billing history": "Không thể tải lịch sử thanh toán",
+ "Failed to load enabled models": "Không thể tải các mô hình đã bật",
"Failed to load home page content": "Không thể tải nội dung trang chủ",
"Failed to load image": "Không thể tải ảnh",
"Failed to load key status": "Không thể tải trạng thái khóa",
diff --git a/web/default/src/i18n/locales/zh-TW.json b/web/default/src/i18n/locales/zh-TW.json
index 26addcd7..279e4e6e 100644
--- a/web/default/src/i18n/locales/zh-TW.json
+++ b/web/default/src/i18n/locales/zh-TW.json
@@ -1845,6 +1845,7 @@
"Failed to load": "載入失敗",
"Failed to load API keys": "載入 API 金鑰失敗",
"Failed to load billing history": "載入收費歷史失敗",
+ "Failed to load enabled models": "獲取啟用模型失敗",
"Failed to load home page content": "載入首頁內容失敗",
"Failed to load image": "無法載入圖像",
"Failed to load key status": "載入金鑰狀態失敗",
diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json
index 3da71a04..45368da4 100644
--- a/web/default/src/i18n/locales/zh.json
+++ b/web/default/src/i18n/locales/zh.json
@@ -1845,6 +1845,7 @@
"Failed to load": "加载失败",
"Failed to load API keys": "加载 API 密钥失败",
"Failed to load billing history": "加载计费历史失败",
+ "Failed to load enabled models": "获取启用模型失败",
"Failed to load home page content": "加载首页内容失败",
"Failed to load image": "无法加载图像",
"Failed to load key status": "加载密钥状态失败",