feat: normalize number handling in model pricing editor #3246

This commit is contained in:
CaIon
2026-03-14 15:29:47 +08:00
parent 4e1b05e987
commit 092ee07e94

View File

@@ -59,6 +59,11 @@ const formatNumber = (value) => {
return parseFloat(num.toFixed(12)).toString(); return parseFloat(num.toFixed(12)).toString();
}; };
const toNormalizedNumber = (value) => {
const formatted = formatNumber(value);
return formatted === '' ? null : Number(formatted);
};
const parseOptionJSON = (rawValue) => { const parseOptionJSON = (rawValue) => {
if (!rawValue || rawValue.trim() === '') { if (!rawValue || rawValue.trim() === '') {
return {}; return {};
@@ -123,7 +128,11 @@ const buildModelState = (name, sourceMaps) => {
lockedCompletionRatio: completionRatioMeta.ratio, lockedCompletionRatio: completionRatioMeta.ratio,
completionPrice: completionPrice:
inputPriceNumber !== null && inputPriceNumber !== null &&
hasValue(completionRatioMeta.locked ? completionRatioMeta.ratio : completionRatio) hasValue(
completionRatioMeta.locked
? completionRatioMeta.ratio
: completionRatio,
)
? formatNumber( ? formatNumber(
inputPriceNumber * inputPriceNumber *
Number( Number(
@@ -192,7 +201,9 @@ export const getModelWarnings = (model, t) => {
].some(hasValue); ].some(hasValue);
if (model.hasConflict) { if (model.hasConflict) {
warnings.push(t('当前模型同时存在按次价格和倍率配置,保存时会按当前计费方式覆盖。')); warnings.push(
t('当前模型同时存在按次价格和倍率配置,保存时会按当前计费方式覆盖。'),
);
} }
if ( if (
@@ -207,11 +218,17 @@ export const getModelWarnings = (model, t) => {
].some(hasValue) ].some(hasValue)
) { ) {
warnings.push( warnings.push(
t('当前模型存在未显式设置输入倍率的扩展倍率;填写输入价格后会自动换算为价格字段。'), t(
'当前模型存在未显式设置输入倍率的扩展倍率;填写输入价格后会自动换算为价格字段。',
),
); );
} }
if (model.billingMode === 'per-token' && hasDerivedPricing && !hasValue(model.inputPrice)) { if (
model.billingMode === 'per-token' &&
hasDerivedPricing &&
!hasValue(model.inputPrice)
) {
warnings.push(t('按量计费下需要先填写输入价格,才能保存其它价格项。')); warnings.push(t('按量计费下需要先填写输入价格,才能保存其它价格项。'));
} }
@@ -249,7 +266,8 @@ export const buildSummaryText = (model, t) => {
}; };
export const buildOptionalFieldToggles = (model) => ({ export const buildOptionalFieldToggles = (model) => ({
completionPrice: model.completionRatioLocked || hasValue(model.completionPrice), completionPrice:
model.completionRatioLocked || hasValue(model.completionPrice),
cachePrice: hasValue(model.cachePrice), cachePrice: hasValue(model.cachePrice),
createCachePrice: hasValue(model.createCachePrice), createCachePrice: hasValue(model.createCachePrice),
imagePrice: hasValue(model.imagePrice), imagePrice: hasValue(model.imagePrice),
@@ -271,7 +289,7 @@ const serializeModel = (model, t) => {
if (model.billingMode === 'per-request') { if (model.billingMode === 'per-request') {
if (hasValue(model.fixedPrice)) { if (hasValue(model.fixedPrice)) {
result.ModelPrice = Number(model.fixedPrice); result.ModelPrice = toNormalizedNumber(model.fixedPrice);
} }
return result; return result;
} }
@@ -296,57 +314,68 @@ const serializeModel = (model, t) => {
if (inputPrice === null) { if (inputPrice === null) {
if (hasDependentPrice) { if (hasDependentPrice) {
throw new Error( throw new Error(
t('模型 {{name}} 缺少输入价格,无法计算补全/缓存/图片/音频价格对应的倍率', { t(
name: model.name, '模型 {{name}} 缺少输入价格,无法计算补全/缓存/图片/音频价格对应的倍率',
}), {
name: model.name,
},
),
); );
} }
if (hasValue(model.rawRatios.modelRatio)) { if (hasValue(model.rawRatios.modelRatio)) {
result.ModelRatio = Number(model.rawRatios.modelRatio); result.ModelRatio = toNormalizedNumber(model.rawRatios.modelRatio);
} }
if (hasValue(model.rawRatios.completionRatio)) { if (hasValue(model.rawRatios.completionRatio)) {
result.CompletionRatio = Number(model.rawRatios.completionRatio); result.CompletionRatio = toNormalizedNumber(
model.rawRatios.completionRatio,
);
} }
if (hasValue(model.rawRatios.cacheRatio)) { if (hasValue(model.rawRatios.cacheRatio)) {
result.CacheRatio = Number(model.rawRatios.cacheRatio); result.CacheRatio = toNormalizedNumber(model.rawRatios.cacheRatio);
} }
if (hasValue(model.rawRatios.createCacheRatio)) { if (hasValue(model.rawRatios.createCacheRatio)) {
result.CreateCacheRatio = Number(model.rawRatios.createCacheRatio); result.CreateCacheRatio = toNormalizedNumber(
model.rawRatios.createCacheRatio,
);
} }
if (hasValue(model.rawRatios.imageRatio)) { if (hasValue(model.rawRatios.imageRatio)) {
result.ImageRatio = Number(model.rawRatios.imageRatio); result.ImageRatio = toNormalizedNumber(model.rawRatios.imageRatio);
} }
if (hasValue(model.rawRatios.audioRatio)) { if (hasValue(model.rawRatios.audioRatio)) {
result.AudioRatio = Number(model.rawRatios.audioRatio); result.AudioRatio = toNormalizedNumber(model.rawRatios.audioRatio);
} }
if (hasValue(model.rawRatios.audioCompletionRatio)) { if (hasValue(model.rawRatios.audioCompletionRatio)) {
result.AudioCompletionRatio = Number(model.rawRatios.audioCompletionRatio); result.AudioCompletionRatio = toNormalizedNumber(
model.rawRatios.audioCompletionRatio,
);
} }
return result; return result;
} }
result.ModelRatio = inputPrice / 2; result.ModelRatio = toNormalizedNumber(inputPrice / 2);
if (!model.completionRatioLocked && completionPrice !== null) { if (!model.completionRatioLocked && completionPrice !== null) {
result.CompletionRatio = completionPrice / inputPrice; result.CompletionRatio = toNormalizedNumber(completionPrice / inputPrice);
} else if ( } else if (
model.completionRatioLocked && model.completionRatioLocked &&
hasValue(model.rawRatios.completionRatio) hasValue(model.rawRatios.completionRatio)
) { ) {
result.CompletionRatio = Number(model.rawRatios.completionRatio); result.CompletionRatio = toNormalizedNumber(
model.rawRatios.completionRatio,
);
} }
if (cachePrice !== null) { if (cachePrice !== null) {
result.CacheRatio = cachePrice / inputPrice; result.CacheRatio = toNormalizedNumber(cachePrice / inputPrice);
} }
if (createCachePrice !== null) { if (createCachePrice !== null) {
result.CreateCacheRatio = createCachePrice / inputPrice; result.CreateCacheRatio = toNormalizedNumber(createCachePrice / inputPrice);
} }
if (imagePrice !== null) { if (imagePrice !== null) {
result.ImageRatio = imagePrice / inputPrice; result.ImageRatio = toNormalizedNumber(imagePrice / inputPrice);
} }
if (audioInputPrice !== null) { if (audioInputPrice !== null) {
result.AudioRatio = audioInputPrice / inputPrice; result.AudioRatio = toNormalizedNumber(audioInputPrice / inputPrice);
} }
if (audioOutputPrice !== null) { if (audioOutputPrice !== null) {
if (audioInputPrice === null || audioInputPrice === 0) { if (audioInputPrice === null || audioInputPrice === 0) {
@@ -356,7 +385,9 @@ const serializeModel = (model, t) => {
}), }),
); );
} }
result.AudioCompletionRatio = audioOutputPrice / audioInputPrice; result.AudioCompletionRatio = toNormalizedNumber(
audioOutputPrice / audioInputPrice,
);
} }
return result; return result;
@@ -455,7 +486,8 @@ export const buildPreviewRows = (model, t) => {
{ {
key: 'CacheRatio', key: 'CacheRatio',
label: 'CacheRatio', label: 'CacheRatio',
value: cachePrice !== null ? formatNumber(cachePrice / inputPrice) : t('空'), value:
cachePrice !== null ? formatNumber(cachePrice / inputPrice) : t('空'),
}, },
{ {
key: 'CreateCacheRatio', key: 'CreateCacheRatio',
@@ -468,7 +500,8 @@ export const buildPreviewRows = (model, t) => {
{ {
key: 'ImageRatio', key: 'ImageRatio',
label: 'ImageRatio', label: 'ImageRatio',
value: imagePrice !== null ? formatNumber(imagePrice / inputPrice) : t('空'), value:
imagePrice !== null ? formatNumber(imagePrice / inputPrice) : t('空'),
}, },
{ {
key: 'AudioRatio', key: 'AudioRatio',
@@ -482,7 +515,9 @@ export const buildPreviewRows = (model, t) => {
key: 'AudioCompletionRatio', key: 'AudioCompletionRatio',
label: 'AudioCompletionRatio', label: 'AudioCompletionRatio',
value: value:
audioOutputPrice !== null && audioInputPrice !== null && audioInputPrice !== 0 audioOutputPrice !== null &&
audioInputPrice !== null &&
audioInputPrice !== 0
? formatNumber(audioOutputPrice / audioInputPrice) ? formatNumber(audioOutputPrice / audioInputPrice)
: t('空'), : t('空'),
}, },
@@ -585,7 +620,8 @@ export function useModelPricingEditorState({
}, [currentPage, filteredModels]); }, [currentPage, filteredModels]);
const selectedModel = useMemo( const selectedModel = useMemo(
() => visibleModels.find((model) => model.name === selectedModelName) || null, () =>
visibleModels.find((model) => model.name === selectedModelName) || null,
[selectedModelName, visibleModels], [selectedModelName, visibleModels],
); );
@@ -605,7 +641,9 @@ export function useModelPricingEditorState({
useEffect(() => { useEffect(() => {
setSelectedModelNames((previous) => setSelectedModelNames((previous) =>
previous.filter((name) => visibleModels.some((model) => model.name === name)), previous.filter((name) =>
visibleModels.some((model) => model.name === name),
),
); );
}, [visibleModels]); }, [visibleModels]);
@@ -779,7 +817,9 @@ export function useModelPricingEditorState({
delete next[name]; delete next[name];
return next; return next;
}); });
setSelectedModelNames((previous) => previous.filter((item) => item !== name)); setSelectedModelNames((previous) =>
previous.filter((item) => item !== name),
);
if (selectedModelName === name) { if (selectedModelName === name) {
setSelectedModelName(nextModels[0]?.name || ''); setSelectedModelName(nextModels[0]?.name || '');
} }
@@ -823,7 +863,8 @@ export function useModelPricingEditorState({
hasValue(nextModel.lockedCompletionRatio) hasValue(nextModel.lockedCompletionRatio)
) { ) {
nextModel.completionPrice = formatNumber( nextModel.completionPrice = formatNumber(
Number(nextModel.inputPrice) * Number(nextModel.lockedCompletionRatio), Number(nextModel.inputPrice) *
Number(nextModel.lockedCompletionRatio),
); );
} }