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": "加载密钥状态失败",