feat: add billing expression system documentation and enhance tiered billing logic
- Introduced a new rule for the Billing Expression System, emphasizing the importance of reading `pkg/billingexpr/expr.md` for dynamic billing. - Updated the billing expression logic to support new variables and improved handling of image and audio tokens. - Enhanced the tiered billing functionality with versioning support for expressions and refined quota calculations. - Added tests to validate the new billing expression features and ensure correctness in pricing calculations.
This commit is contained in:
+2
-10
@@ -21,6 +21,7 @@ import React from 'react';
|
||||
import { Card, Avatar, Tag, Table, Typography } from '@douyinfe/semi-ui';
|
||||
import { IconPriceTag } from '@douyinfe/semi-icons';
|
||||
import { parseTiersFromExpr } from '../../../../../helpers';
|
||||
import { BILLING_VARS } from '../../../../../constants';
|
||||
import {
|
||||
splitBillingExprAndRequestRules,
|
||||
tryParseRequestRuleExpr,
|
||||
@@ -113,16 +114,7 @@ export default function DynamicPricingBreakdown({ billingExpr, t }) {
|
||||
);
|
||||
}
|
||||
|
||||
const priceFields = [
|
||||
['inputPrice', '输入价格'],
|
||||
['outputPrice', '补全价格'],
|
||||
['cacheReadPrice', '缓存读取'],
|
||||
['cacheCreatePrice', '缓存创建'],
|
||||
['cacheCreate1hPrice', '缓存创建-1h'],
|
||||
['imagePrice', '图片输入'],
|
||||
['audioInputPrice', '音频输入'],
|
||||
['audioOutputPrice', '音频输出'],
|
||||
];
|
||||
const priceFields = BILLING_VARS.map((v) => [v.field, v.shortLabel]);
|
||||
|
||||
const tierColumns = [
|
||||
{
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Single source of truth for billing expression variables.
|
||||
*
|
||||
* Every expression variable (p, c, cr, cc, ...) is defined here once.
|
||||
* All frontend consumers — editor, estimator, log display, model detail —
|
||||
* derive their data structures from this registry.
|
||||
*
|
||||
* To add a new variable:
|
||||
* 1. Add an entry here
|
||||
* 2. Backend: add to TokenParams, compileEnvPrototype, runProgram env, BuildTieredTokenParams
|
||||
*/
|
||||
|
||||
export const BILLING_VARS = [
|
||||
{ key: 'p', field: 'inputPrice', tierField: 'input_unit_cost', label: '输入价格', shortLabel: '输入', side: 'input', isBase: true },
|
||||
{ key: 'c', field: 'outputPrice', tierField: 'output_unit_cost', label: '补全价格', shortLabel: '补全', side: 'output', isBase: true },
|
||||
{ key: 'cr', field: 'cacheReadPrice', tierField: 'cache_read_unit_cost', label: '缓存读取价格', shortLabel: '缓存读', side: 'input', group: 'cache' },
|
||||
{ key: 'cc', field: 'cacheCreatePrice', tierField: 'cache_create_unit_cost', label: '缓存创建价格', shortLabel: '缓存创建', side: 'input', group: 'cache' },
|
||||
{ key: 'cc1h', field: 'cacheCreate1hPrice', tierField: 'cache_create_1h_unit_cost', label: '1h缓存创建价格', shortLabel: '1h缓存创建', side: 'input', group: 'cache' },
|
||||
{ key: 'img', field: 'imagePrice', tierField: 'image_unit_cost', label: '图片输入价格', shortLabel: '图片输入', side: 'input', group: 'media' },
|
||||
{ key: 'img_o', field: 'imageOutputPrice', tierField: 'image_output_unit_cost', label: '图片输出价格', shortLabel: '图片输出', side: 'output', group: 'media' },
|
||||
{ key: 'ai', field: 'audioInputPrice', tierField: 'audio_input_unit_cost', label: '音频输入价格', shortLabel: '音频输入', side: 'input', group: 'media' },
|
||||
{ key: 'ao', field: 'audioOutputPrice', tierField: 'audio_output_unit_cost', label: '音频补全价格', shortLabel: '音频输出', side: 'output', group: 'media' },
|
||||
];
|
||||
|
||||
export const BILLING_VAR_KEYS = BILLING_VARS.map((v) => v.key);
|
||||
|
||||
export const BILLING_EXTRA_VARS = BILLING_VARS.filter((v) => !v.isBase);
|
||||
|
||||
export const BILLING_VAR_KEY_TO_FIELD = Object.fromEntries(
|
||||
BILLING_VARS.map((v) => [v.key, v.field]),
|
||||
);
|
||||
|
||||
export const BILLING_VAR_FIELD_TO_LABEL = Object.fromEntries(
|
||||
BILLING_VARS.map((v) => [v.field, v.label]),
|
||||
);
|
||||
|
||||
export const BILLING_VAR_FIELD_TO_SHORT_LABEL = Object.fromEntries(
|
||||
BILLING_VARS.map((v) => [v.field, v.shortLabel]),
|
||||
);
|
||||
|
||||
export const BILLING_CACHE_VAR_MAP = BILLING_EXTRA_VARS.map((v) => ({
|
||||
field: v.tierField,
|
||||
exprVar: v.key,
|
||||
}));
|
||||
|
||||
export const BILLING_VAR_REGEX = new RegExp(
|
||||
`\\b(${BILLING_VAR_KEYS.join('|')})\\s*\\*\\s*([\\d.eE+-]+)`,
|
||||
'g',
|
||||
);
|
||||
Vendored
+1
@@ -25,3 +25,4 @@ export * from './dashboard.constants';
|
||||
export * from './playground.constants';
|
||||
export * from './redemption.constants';
|
||||
export * from './channel-affinity-template.constants';
|
||||
export * from './billing.constants';
|
||||
|
||||
Vendored
+17
-29
@@ -21,6 +21,11 @@ import i18next from 'i18next';
|
||||
import { Modal, Tag, Typography, Avatar } from '@douyinfe/semi-ui';
|
||||
import { copy, showSuccess } from './utils';
|
||||
import { MOBILE_BREAKPOINT } from '../hooks/common/useIsMobile';
|
||||
import {
|
||||
BILLING_VARS,
|
||||
BILLING_VAR_KEY_TO_FIELD,
|
||||
BILLING_VAR_REGEX,
|
||||
} from '../constants';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import * as LobeIcons from '@lobehub/icons';
|
||||
import {
|
||||
@@ -2210,22 +2215,22 @@ export function renderLogContent(opts) {
|
||||
}
|
||||
}
|
||||
|
||||
const TIER_VAR_KEYS = ['p', 'c', 'cr', 'cc', 'cc1h', 'img', 'ai', 'ao'];
|
||||
const TIER_VAR_TO_FIELD = {
|
||||
p: 'inputPrice', c: 'outputPrice',
|
||||
cr: 'cacheReadPrice', cc: 'cacheCreatePrice', cc1h: 'cacheCreate1hPrice',
|
||||
img: 'imagePrice', ai: 'audioInputPrice', ao: 'audioOutputPrice',
|
||||
};
|
||||
export function stripExprVersion(exprStr) {
|
||||
if (!exprStr) return { version: 1, body: '' };
|
||||
const m = exprStr.match(/^v(\d+):([\s\S]*)$/);
|
||||
if (m) return { version: Number(m[1]), body: m[2] };
|
||||
return { version: 1, body: exprStr };
|
||||
}
|
||||
|
||||
function parseTierBody(bodyStr) {
|
||||
const coeffs = {};
|
||||
const re = new RegExp(`\\b(${TIER_VAR_KEYS.join('|')})\\s*\\*\\s*([\\d.eE+-]+)`, 'g');
|
||||
const re = new RegExp(BILLING_VAR_REGEX.source, 'g');
|
||||
let m;
|
||||
while ((m = re.exec(bodyStr)) !== null) {
|
||||
if (!(m[1] in coeffs)) coeffs[m[1]] = Number(m[2]);
|
||||
}
|
||||
const tier = {};
|
||||
for (const [varName, field] of Object.entries(TIER_VAR_TO_FIELD)) {
|
||||
for (const [varName, field] of Object.entries(BILLING_VAR_KEY_TO_FIELD)) {
|
||||
tier[field] = coeffs[varName] || 0;
|
||||
}
|
||||
return tier;
|
||||
@@ -2234,11 +2239,12 @@ function parseTierBody(bodyStr) {
|
||||
export function parseTiersFromExpr(exprStr) {
|
||||
if (!exprStr) return [];
|
||||
try {
|
||||
const { body } = stripExprVersion(exprStr);
|
||||
const condGroup = `((?:(?:p|c)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)(?:\\s*&&\\s*(?:p|c)\\s*(?:<|<=|>|>=)\\s*[\\d.eE+]+)*)`;
|
||||
const tierRe = new RegExp(`(?:${condGroup}\\s*\\?\\s*)?tier\\("([^"]*)",\\s*([^)]+)\\)`, 'g');
|
||||
const tiers = [];
|
||||
let m;
|
||||
while ((m = tierRe.exec(exprStr)) !== null) {
|
||||
while ((m = tierRe.exec(body)) !== null) {
|
||||
const condStr = m[1] || '';
|
||||
const conditions = [];
|
||||
if (condStr) {
|
||||
@@ -2281,16 +2287,7 @@ export function renderTieredModelPrice(opts) {
|
||||
const { symbol, rate } = getCurrencyConfig();
|
||||
const gr = groupRatio || 1;
|
||||
|
||||
const priceLines = [
|
||||
['inputPrice', '输入价格'],
|
||||
['outputPrice', '补全价格'],
|
||||
['cacheReadPrice', '缓存读取价格'],
|
||||
['cacheCreatePrice', '缓存创建价格'],
|
||||
['cacheCreate1hPrice', '1h缓存创建价格'],
|
||||
['imagePrice', '图片输入价格'],
|
||||
['audioInputPrice', '音频输入价格'],
|
||||
['audioOutputPrice', '音频输出价格'],
|
||||
];
|
||||
const priceLines = BILLING_VARS.map((v) => [v.field, v.label]);
|
||||
|
||||
const lines = [
|
||||
buildBillingText('命中档位:{{tier}}', { tier: matchedTier || tier.label }),
|
||||
@@ -2331,16 +2328,7 @@ export function renderTieredModelPriceSimple(opts) {
|
||||
];
|
||||
|
||||
if (tier && isPriceDisplayMode(displayMode)) {
|
||||
const priceSegments = [
|
||||
['inputPrice', '输入'],
|
||||
['outputPrice', '补全'],
|
||||
['cacheReadPrice', '缓存读'],
|
||||
['cacheCreatePrice', '缓存创建'],
|
||||
['cacheCreate1hPrice', '1h缓存创建'],
|
||||
['imagePrice', '图片输入'],
|
||||
['audioInputPrice', '音频输入'],
|
||||
['audioOutputPrice', '音频输出'],
|
||||
];
|
||||
const priceSegments = BILLING_VARS.map((v) => [v.field, v.shortLabel]);
|
||||
for (const [field, label] of priceSegments) {
|
||||
if (tier[field] > 0) {
|
||||
segments.push({
|
||||
|
||||
Vendored
+8
-16
@@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import { Toast, Pagination } from '@douyinfe/semi-ui';
|
||||
import { toastConstants } from '../constants';
|
||||
import { toastConstants, BILLING_VARS, BILLING_VAR_REGEX } from '../constants';
|
||||
import React from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
@@ -901,30 +901,22 @@ export const formatDynamicPriceSummary = (billingExpr, t, groupRatio = 1) => {
|
||||
if (!billingExpr) return <span style={{ color: 'var(--semi-color-text-1)' }}>{t('动态计费')}</span>;
|
||||
|
||||
const gr = groupRatio || 1;
|
||||
const tierMatches = billingExpr.match(/tier\(/g) || [];
|
||||
const exprBody = billingExpr.replace(/^v\d+:/, '');
|
||||
const tierMatches = exprBody.match(/tier\(/g) || [];
|
||||
const tierCount = tierMatches.length;
|
||||
|
||||
const varCoeffs = {};
|
||||
const varRe = /\b(p|c|cr|cc|cc1h|img|ai|ao)\s*\*\s*([\d.eE+-]+)/g;
|
||||
const varRe = new RegExp(BILLING_VAR_REGEX.source, 'g');
|
||||
let vm;
|
||||
while ((vm = varRe.exec(billingExpr)) !== null) {
|
||||
while ((vm = varRe.exec(exprBody)) !== null) {
|
||||
if (!(vm[1] in varCoeffs)) varCoeffs[vm[1]] = Number(vm[2]);
|
||||
}
|
||||
const hasCoeffs = 'p' in varCoeffs || 'c' in varCoeffs;
|
||||
|
||||
const varLabels = [
|
||||
['p', '输入价格'],
|
||||
['c', '补全价格'],
|
||||
['cr', '缓存读取价格'],
|
||||
['cc', '缓存创建价格'],
|
||||
['cc1h', '1h缓存创建价格'],
|
||||
['img', '图片输入价格'],
|
||||
['ai', '音频输入价格'],
|
||||
['ao', '音频输出价格'],
|
||||
];
|
||||
const varLabels = BILLING_VARS.map((v) => [v.key, v.label]);
|
||||
|
||||
const hasTimeCondition = /\b(?:hour|weekday|month|day)\(/.test(billingExpr);
|
||||
const hasRequestCondition = /\b(?:param|header)\(/.test(billingExpr);
|
||||
const hasTimeCondition = /\b(?:hour|weekday|month|day)\(/.test(exprBody);
|
||||
const hasRequestCondition = /\b(?:param|header)\(/.test(exprBody);
|
||||
|
||||
const tags = [];
|
||||
if (tierCount > 1) tags.push(`${tierCount}${t('档')}`);
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { IconDelete, IconPlus } from '@douyinfe/semi-icons';
|
||||
import { renderQuota } from '../../../../helpers/render';
|
||||
import { BILLING_EXTRA_VARS, BILLING_CACHE_VAR_MAP } from '../../../../constants';
|
||||
import {
|
||||
createEmptyCondition,
|
||||
createEmptyTimeCondition,
|
||||
@@ -96,15 +97,7 @@ function buildConditionStr(conditions) {
|
||||
.join(' && ');
|
||||
}
|
||||
|
||||
// CACHE_VAR_MAP maps tier data fields to Expr variable names (cache + image + audio)
|
||||
const CACHE_VAR_MAP = [
|
||||
{ field: 'cache_read_unit_cost', exprVar: 'cr' },
|
||||
{ field: 'cache_create_unit_cost', exprVar: 'cc' },
|
||||
{ field: 'cache_create_1h_unit_cost', exprVar: 'cc1h' },
|
||||
{ field: 'image_unit_cost', exprVar: 'img' },
|
||||
{ field: 'audio_input_unit_cost', exprVar: 'ai' },
|
||||
{ field: 'audio_output_unit_cost', exprVar: 'ao' },
|
||||
];
|
||||
const CACHE_VAR_MAP = BILLING_CACHE_VAR_MAP;
|
||||
|
||||
function getTierCacheMode(tier) {
|
||||
if (tier?.cache_mode === CACHE_MODE_TIMED) {
|
||||
@@ -197,6 +190,8 @@ function generateExprFromVisualConfig(config) {
|
||||
function tryParseVisualConfig(exprStr) {
|
||||
if (!exprStr) return null;
|
||||
try {
|
||||
const versionMatch = exprStr.match(/^v\d+:([\s\S]*)$/);
|
||||
if (versionMatch) exprStr = versionMatch[1];
|
||||
const cacheVarNames = CACHE_VAR_MAP.map((cv) => cv.exprVar);
|
||||
const optCacheStr = cacheVarNames
|
||||
.map((v) => `(?:\\s*\\+\\s*${v}\\s*\\*\\s*([\\d.eE+-]+))?`)
|
||||
@@ -385,7 +380,8 @@ const CACHE_FIELDS_GENERIC = [
|
||||
];
|
||||
|
||||
function ExtendedPriceBlock({ tier, index, onUpdate, t }) {
|
||||
const hasAny = [...CACHE_FIELDS_TIMED, 'image_unit_cost', 'audio_input_unit_cost', 'audio_output_unit_cost'].some(
|
||||
const mediaFields = BILLING_EXTRA_VARS.filter((v) => v.group === 'media');
|
||||
const hasAny = [...CACHE_FIELDS_TIMED, ...mediaFields.map((v) => v.tierField)].some(
|
||||
(f) => Number(tier[typeof f === 'string' ? f : f.field]) > 0,
|
||||
);
|
||||
const [expanded, setExpanded] = useState(hasAny);
|
||||
@@ -464,15 +460,11 @@ function ExtendedPriceBlock({ tier, index, onUpdate, t }) {
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr 1fr',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
{[
|
||||
{ field: 'image_unit_cost', labelKey: '图片输入价格' },
|
||||
{ field: 'audio_input_unit_cost', labelKey: '音频输入价格' },
|
||||
{ field: 'audio_output_unit_cost', labelKey: '音频补全价格' },
|
||||
].map((cf) => (
|
||||
{mediaFields.map((v) => ({ field: v.tierField, labelKey: v.label })).map((cf) => (
|
||||
<div key={cf.field}>
|
||||
<Text
|
||||
size='small'
|
||||
@@ -779,6 +771,8 @@ const PRESET_GROUPS = [
|
||||
group: '多模态',
|
||||
presets: [
|
||||
{ key: 'gpt-image-1-mini', label: 'GPT-Image-1-Mini', expr: 'tier("base", p * 2 + c * 8 + img * 2.5)' },
|
||||
{ key: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', expr: 'tier("base", p * 0.3 + c * 2.5 + cr * 0.03 + ai * 1.0)' },
|
||||
{ key: 'gemini-3-pro-image', label: 'Gemini 3 Pro Image', expr: 'tier("base", p * 2 + c * 12 + img_o * 120)' },
|
||||
{ key: 'qwen3-omni-flash', label: 'Qwen3-Omni-Flash', expr: 'tier("base", p * 0.43 + c * 3.06 + img * 0.78 + ai * 3.81 + ao * 15.11)' },
|
||||
],
|
||||
},
|
||||
@@ -913,45 +907,22 @@ function RawExprEditor({ exprString, onChange, t }) {
|
||||
// Cache token inputs for estimator — auto-shown when expression uses cache vars
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const EXTRA_ESTIMATOR_FIELDS = [
|
||||
{ var: 'cr', stateKey: 'cacheReadTokens', labelKey: '缓存读取 Token (cr)' },
|
||||
{ var: 'cc', stateKey: 'cacheCreateTokens', labelKey: '缓存创建 Token (cc)' },
|
||||
{ var: 'cc1h', stateKey: 'cacheCreate1hTokens', labelKey: '缓存创建-1小时 (cc1h)' },
|
||||
{ var: 'img', stateKey: 'imageTokens', labelKey: '图片输入 Token (img)' },
|
||||
{ var: 'ai', stateKey: 'audioInputTokens', labelKey: '音频输入 Token (ai)' },
|
||||
{ var: 'ao', stateKey: 'audioOutputTokens', labelKey: '音频补全 Token (ao)' },
|
||||
];
|
||||
const EXTRA_ESTIMATOR_FIELDS = BILLING_EXTRA_VARS.map((v) => ({
|
||||
var: v.key,
|
||||
stateKey: v.field.replace('Price', 'Tokens'),
|
||||
labelKey: `${v.shortLabel} Token (${v.key})`,
|
||||
}));
|
||||
|
||||
function CacheTokenEstimatorInputs({
|
||||
effectiveExpr,
|
||||
cacheReadTokens, setCacheReadTokens,
|
||||
cacheCreateTokens, setCacheCreateTokens,
|
||||
cacheCreate1hTokens, setCacheCreate1hTokens,
|
||||
imageTokens, setImageTokens,
|
||||
audioInputTokens, setAudioInputTokens,
|
||||
audioOutputTokens, setAudioOutputTokens,
|
||||
extraTokenValues,
|
||||
extraTokenSetters,
|
||||
t,
|
||||
}) {
|
||||
const setters = {
|
||||
cacheReadTokens: setCacheReadTokens,
|
||||
cacheCreateTokens: setCacheCreateTokens,
|
||||
cacheCreate1hTokens: setCacheCreate1hTokens,
|
||||
imageTokens: setImageTokens,
|
||||
audioInputTokens: setAudioInputTokens,
|
||||
audioOutputTokens: setAudioOutputTokens,
|
||||
};
|
||||
const values = {
|
||||
cacheReadTokens,
|
||||
cacheCreateTokens,
|
||||
cacheCreate1hTokens,
|
||||
imageTokens,
|
||||
audioInputTokens,
|
||||
audioOutputTokens,
|
||||
};
|
||||
|
||||
const usesExtra = useMemo(() => {
|
||||
if (!effectiveExpr) return false;
|
||||
return /\b(cr|cc1h|cc|img|ai|ao)\b/.test(effectiveExpr);
|
||||
const varNames = EXTRA_ESTIMATOR_FIELDS.map((f) => f.var.replace('_', '_')).join('|');
|
||||
return new RegExp(`\\b(${varNames})\\b`).test(effectiveExpr);
|
||||
}, [effectiveExpr]);
|
||||
|
||||
if (!usesExtra) return null;
|
||||
@@ -971,9 +942,9 @@ function CacheTokenEstimatorInputs({
|
||||
{t(cf.labelKey)}
|
||||
</Text>
|
||||
<InputNumber
|
||||
value={values[cf.stateKey]}
|
||||
value={extraTokenValues[cf.stateKey]}
|
||||
min={0}
|
||||
onChange={(val) => setters[cf.stateKey](val ?? 0)}
|
||||
onChange={(val) => extraTokenSetters[cf.stateKey](val ?? 0)}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
@@ -986,37 +957,17 @@ function CacheTokenEstimatorInputs({
|
||||
// Cost estimator (works with any Expr string)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function evalExprLocally(exprStr, p, c, cr, cc, cc1h, img, ai, ao) {
|
||||
function evalExprLocally(exprStr, p, c, extraTokenValues) {
|
||||
try {
|
||||
let matchedTier = '';
|
||||
const tierFn = (name, value) => {
|
||||
matchedTier = name;
|
||||
return value;
|
||||
};
|
||||
const env = {
|
||||
p,
|
||||
c,
|
||||
cr: cr || 0,
|
||||
cc: cc || 0,
|
||||
cc1h: cc1h || 0,
|
||||
img: img || 0,
|
||||
ai: ai || 0,
|
||||
ao: ao || 0,
|
||||
prompt_tokens: p,
|
||||
completion_tokens: c,
|
||||
cache_read_tokens: cr || 0,
|
||||
cache_create_tokens: cc || 0,
|
||||
cache_create_1h_tokens: cc1h || 0,
|
||||
image_tokens: img || 0,
|
||||
audio_input_tokens: ai || 0,
|
||||
audio_output_tokens: ao || 0,
|
||||
tier: tierFn,
|
||||
max: Math.max,
|
||||
min: Math.min,
|
||||
abs: Math.abs,
|
||||
ceil: Math.ceil,
|
||||
floor: Math.floor,
|
||||
};
|
||||
const env = { p, c, tier: tierFn, max: Math.max, min: Math.min, abs: Math.abs, ceil: Math.ceil, floor: Math.floor };
|
||||
for (const field of EXTRA_ESTIMATOR_FIELDS) {
|
||||
env[field.var] = extraTokenValues[field.stateKey] || 0;
|
||||
}
|
||||
const fn = new Function(
|
||||
...Object.keys(env),
|
||||
`"use strict"; return (${exprStr});`,
|
||||
@@ -1281,6 +1232,7 @@ export default function TieredPricingEditor({ model, onExprChange, requestRuleEx
|
||||
const [cacheCreateTokens, setCacheCreateTokens] = useState(0);
|
||||
const [cacheCreate1hTokens, setCacheCreate1hTokens] = useState(0);
|
||||
const [imageTokens, setImageTokens] = useState(0);
|
||||
const [imageOutputTokens, setImageOutputTokens] = useState(0);
|
||||
const [audioInputTokens, setAudioInputTokens] = useState(0);
|
||||
const [audioOutputTokens, setAudioOutputTokens] = useState(0);
|
||||
|
||||
@@ -1389,15 +1341,27 @@ export default function TieredPricingEditor({ model, onExprChange, requestRuleEx
|
||||
[onRequestRuleExprChange],
|
||||
);
|
||||
|
||||
const evalResult = useMemo(
|
||||
() => evalExprLocally(
|
||||
effectiveExpr, promptTokens, completionTokens,
|
||||
cacheReadTokens, cacheCreateTokens, cacheCreate1hTokens,
|
||||
imageTokens, audioInputTokens, audioOutputTokens,
|
||||
),
|
||||
const extraTokenValues = {
|
||||
cacheReadTokens, cacheCreateTokens, cacheCreate1hTokens,
|
||||
imageTokens, imageOutputTokens, audioInputTokens, audioOutputTokens,
|
||||
};
|
||||
const extraTokenSetters = {
|
||||
cacheReadTokens: setCacheReadTokens, cacheCreateTokens: setCacheCreateTokens,
|
||||
cacheCreate1hTokens: setCacheCreate1hTokens, imageTokens: setImageTokens,
|
||||
imageOutputTokens: setImageOutputTokens, audioInputTokens: setAudioInputTokens,
|
||||
audioOutputTokens: setAudioOutputTokens,
|
||||
};
|
||||
|
||||
const evalResult = useMemo(() => {
|
||||
const result = evalExprLocally(effectiveExpr, promptTokens, completionTokens, extraTokenValues);
|
||||
if (!result.error) {
|
||||
result.cost = result.cost / 1000000 * (parseFloat(localStorage.getItem('quota_per_unit')) || 500000);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
[effectiveExpr, promptTokens, completionTokens,
|
||||
cacheReadTokens, cacheCreateTokens, cacheCreate1hTokens,
|
||||
imageTokens, audioInputTokens, audioOutputTokens],
|
||||
imageTokens, imageOutputTokens, audioInputTokens, audioOutputTokens],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -1530,18 +1494,8 @@ export default function TieredPricingEditor({ model, onExprChange, requestRuleEx
|
||||
{/* Cache token inputs — shown when expression uses cache variables */}
|
||||
<CacheTokenEstimatorInputs
|
||||
effectiveExpr={effectiveExpr}
|
||||
cacheReadTokens={cacheReadTokens}
|
||||
setCacheReadTokens={setCacheReadTokens}
|
||||
cacheCreateTokens={cacheCreateTokens}
|
||||
setCacheCreateTokens={setCacheCreateTokens}
|
||||
cacheCreate1hTokens={cacheCreate1hTokens}
|
||||
setCacheCreate1hTokens={setCacheCreate1hTokens}
|
||||
imageTokens={imageTokens}
|
||||
setImageTokens={setImageTokens}
|
||||
audioInputTokens={audioInputTokens}
|
||||
setAudioInputTokens={setAudioInputTokens}
|
||||
audioOutputTokens={audioOutputTokens}
|
||||
setAudioOutputTokens={setAudioOutputTokens}
|
||||
extraTokenValues={extraTokenValues}
|
||||
extraTokenSetters={extraTokenSetters}
|
||||
t={t}
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -646,8 +646,8 @@ export function useModelPricingEditorState({
|
||||
ImageRatio: parseOptionJSON(options.ImageRatio),
|
||||
AudioRatio: parseOptionJSON(options.AudioRatio),
|
||||
AudioCompletionRatio: parseOptionJSON(options.AudioCompletionRatio),
|
||||
ModelBillingMode: parseOptionJSON(options.ModelBillingMode),
|
||||
ModelBillingExpr: parseOptionJSON(options.ModelBillingExpr),
|
||||
ModelBillingMode: parseOptionJSON(options['billing_setting.billing_mode']),
|
||||
ModelBillingExpr: parseOptionJSON(options['billing_setting.billing_expr']),
|
||||
};
|
||||
|
||||
const names = new Set([
|
||||
@@ -1035,19 +1035,19 @@ export function useModelPricingEditorState({
|
||||
};
|
||||
|
||||
const tieredOutput = {
|
||||
ModelBillingMode: {},
|
||||
ModelBillingExpr: {},
|
||||
'billing_setting.billing_mode': {},
|
||||
'billing_setting.billing_expr': {},
|
||||
};
|
||||
|
||||
for (const model of models) {
|
||||
if (model.billingMode === 'tiered_expr') {
|
||||
tieredOutput.ModelBillingMode[model.name] = 'tiered_expr';
|
||||
tieredOutput['billing_setting.billing_mode'][model.name] = 'tiered_expr';
|
||||
const finalBillingExpr = combineBillingExpr(
|
||||
model.billingExpr,
|
||||
model.requestRuleExpr,
|
||||
);
|
||||
if (finalBillingExpr) {
|
||||
tieredOutput.ModelBillingExpr[model.name] = finalBillingExpr;
|
||||
tieredOutput['billing_setting.billing_expr'][model.name] = finalBillingExpr;
|
||||
}
|
||||
}
|
||||
if (model.billingMode === 'tiered_expr') {
|
||||
|
||||
Reference in New Issue
Block a user