|
- export const BILLING_UNITS = {
- perCall: 'per_call',
- per1MTokens: 'per_1m_tokens',
- };
-
- export const PRECONSUME_STRATEGIES = {
- exact: 'exact',
- minimum: 'minimum',
- };
-
- export const FALLBACKS = {
- reject: 'reject',
- max: 'max',
- default: 'default',
- };
-
- export const ROW_SOURCES = {
- manual: 'manual',
- generated: 'generated',
- };
-
- export const DIMENSION_SOURCE_TYPES = {
- request: 'request',
- derived: 'derived',
- advanced: 'advanced',
- };
-
- const RESERVED_KEYS = new Set([
- 'price',
- 'source',
- '__fallback__',
- 'price_formula',
- 'pricing_mode',
- 'fallback_strategy',
- 'schema_version',
- 'scope',
- 'billing_unit',
- 'preconsume_strategy',
- ]);
-
- export const createDefaultConfig = () => ({
- schema_version: 1,
- scope: 'model',
- billing_unit: BILLING_UNITS.perCall,
- preconsume_strategy: PRECONSUME_STRATEGIES.exact,
- dimensions: [
- {
- key: 'resolution',
- source: 'request.resolution',
- type: 'string',
- default: null,
- },
- ],
- table: [
- {
- resolution: '*',
- price: 0.1,
- source: ROW_SOURCES.manual,
- },
- ],
- fallback: {
- strategy: FALLBACKS.reject,
- },
- });
-
- export const validatePricingConfig = (config) => {
- const errors = [];
- if (!config || typeof config !== 'object') {
- return { ok: false, errors: ['config must be an object'] };
- }
-
- if (config.schema_version !== 1) errors.push('schema_version must be 1');
- if (config.scope !== 'model') errors.push('scope must be model');
-
- const billingUnit = config.billing_unit || BILLING_UNITS.perCall;
- const preconsume = config.preconsume_strategy || PRECONSUME_STRATEGIES.exact;
- if (!Object.values(BILLING_UNITS).includes(billingUnit))
- errors.push('billing_unit is invalid');
- if (!Object.values(PRECONSUME_STRATEGIES).includes(preconsume))
- errors.push('preconsume_strategy is invalid');
- if (
- billingUnit === BILLING_UNITS.perCall &&
- preconsume !== PRECONSUME_STRATEGIES.exact
- ) {
- errors.push('per_call requires exact preconsume_strategy');
- }
- if (
- billingUnit === BILLING_UNITS.per1MTokens &&
- preconsume !== PRECONSUME_STRATEGIES.minimum
- ) {
- errors.push('per_1m_tokens requires minimum preconsume_strategy');
- }
-
- if (!Array.isArray(config.dimensions) || config.dimensions.length === 0)
- errors.push('dimensions must not be empty');
- if (!Array.isArray(config.table) || config.table.length === 0)
- errors.push('table must not be empty');
-
- const dimensionKeys = new Set();
- for (const dim of config.dimensions || []) {
- if (!/^[a-z][a-z0-9_]{0,31}$/.test(dim.key || ''))
- errors.push(`dimension key "${dim.key || ''}" is invalid`);
- if (RESERVED_KEYS.has(dim.key))
- errors.push(`dimension key "${dim.key}" is reserved`);
- if (dimensionKeys.has(dim.key))
- errors.push(`dimension key "${dim.key}" is duplicated`);
- dimensionKeys.add(dim.key);
- if (!['string', 'number', 'boolean'].includes(dim.type))
- errors.push(`dimension "${dim.key}" type is invalid`);
- if (dim.optional && (dim.default === null || dim.default === undefined)) {
- errors.push(`optional dimension "${dim.key}" requires a default value`);
- }
- if (!dim.optional && dim.default !== null && dim.default !== undefined) {
- errors.push(`required dimension "${dim.key}" must use null default`);
- }
- if (
- dim.optional &&
- dim.default !== null &&
- dim.default !== undefined &&
- !valueMatchesType(dim.default, dim.type)
- ) {
- errors.push(`dimension "${dim.key}" default does not match type`);
- }
- }
-
- const rowSignatures = new Set();
- const rowPatterns = [];
- for (const [index, row] of (config.table || []).entries()) {
- for (const key of Object.keys(row || {})) {
- if (!dimensionKeys.has(key) && key !== 'price' && key !== 'source')
- errors.push(`row ${index + 1} contains extra field "${key}"`);
- }
- for (const dim of config.dimensions || []) {
- if (!(dim.key in row)) {
- errors.push(`row ${index + 1} missing "${dim.key}"`);
- continue;
- }
- if (row[dim.key] !== '*' && !valueMatchesType(row[dim.key], dim.type)) {
- errors.push(
- `row ${index + 1} value for "${dim.key}" does not match type`,
- );
- }
- }
- const price = Number(row.price);
- if (Number.isNaN(price) || price < 0)
- errors.push(`row ${index + 1} price must be >= 0`);
- if (!Object.values(ROW_SOURCES).includes(row.source))
- errors.push(`row ${index + 1} source must be manual or generated`);
-
- const signature = (config.dimensions || [])
- .map((dim) => String(row[dim.key]))
- .join('\u0001');
- if (rowSignatures.has(signature))
- errors.push(`row ${index + 1} duplicates another row`);
- rowSignatures.add(signature);
- rowPatterns.push({
- index,
- values: (config.dimensions || []).map((dim) => row[dim.key]),
- wildcards: (config.dimensions || []).filter((dim) => row[dim.key] === '*')
- .length,
- });
- }
-
- for (let i = 0; i < rowPatterns.length; i += 1) {
- for (let j = i + 1; j < rowPatterns.length; j += 1) {
- if (rowPatterns[i].wildcards !== rowPatterns[j].wildcards) continue;
- if (patternsOverlap(rowPatterns[i].values, rowPatterns[j].values)) {
- errors.push(
- `rows ${rowPatterns[i].index + 1} and ${rowPatterns[j].index + 1} are ambiguous`,
- );
- }
- }
- }
-
- const strategy = config.fallback?.strategy;
- if (!Object.values(FALLBACKS).includes(strategy))
- errors.push('fallback.strategy is invalid');
- if (
- strategy === FALLBACKS.default &&
- (config.fallback.default_price === undefined ||
- Number(config.fallback.default_price) < 0)
- ) {
- errors.push('fallback.default_price must be >= 0');
- }
-
- return { ok: errors.length === 0, errors };
- };
-
- export const generateRowsFromRatios = ({ dimensions, basePrice, ratios }) => {
- const dims = dimensions || [];
- if (dims.length === 0) return [];
-
- const build = (index, prefix, price) => {
- if (index >= dims.length)
- return [
- {
- ...prefix,
- price: Number(Number(price).toFixed(6)),
- source: ROW_SOURCES.generated,
- },
- ];
- const dim = dims[index];
- const dimRatios = ratios?.[dim.key] || {};
- return Object.entries(dimRatios).flatMap(([value, ratio]) =>
- build(
- index + 1,
- { ...prefix, [dim.key]: parseDimensionValue(value, dim.type) },
- price * Number(ratio || 1),
- ),
- );
- };
-
- return build(0, {}, Number(basePrice || 0));
- };
-
- export const splitDimensionSource = (source) => {
- const text = String(source || '').trim();
- if (text.startsWith('request.'))
- return {
- sourceType: DIMENSION_SOURCE_TYPES.request,
- path: text.slice('request.'.length),
- };
- if (text.startsWith('derived.'))
- return {
- sourceType: DIMENSION_SOURCE_TYPES.derived,
- path: text.slice('derived.'.length),
- };
- return { sourceType: DIMENSION_SOURCE_TYPES.advanced, path: text };
- };
-
- export const composeDimensionSource = (sourceType, path) => {
- const text = String(path || '').trim();
- if (sourceType === DIMENSION_SOURCE_TYPES.request) {
- return text.startsWith('request.') ? text : `request.${text}`;
- }
- if (sourceType === DIMENSION_SOURCE_TYPES.derived) {
- return text.startsWith('derived.') ? text : `derived.${text}`;
- }
- return text;
- };
-
- export const countRatioEntries = (ratios) =>
- Object.values(ratios || {}).reduce(
- (total, values) => total + Object.keys(values || {}).length,
- 0,
- );
-
- export const estimateGeneratedRowCount = (dimensions, ratios) => {
- const dims = dimensions || [];
- if (dims.length === 0) return 0;
- return dims.reduce((total, dim) => {
- const count = Object.keys(ratios?.[dim.key] || {}).length;
- if (count === 0) return 0;
- return total * count;
- }, 1);
- };
-
- export const displayCurrencyToUSD = (amount, currencyConfig) => {
- const rate = Number(currencyConfig?.rate || 1);
- const value = Number(amount || 0);
- if (!Number.isFinite(rate) || rate <= 0) return value;
- return Number((value / rate).toFixed(12));
- };
-
- export const formatDisplayCurrencyNumber = (usdAmount, currencyConfig) => {
- const rate = Number(currencyConfig?.rate || 1);
- const value =
- Number(usdAmount || 0) * (Number.isFinite(rate) && rate > 0 ? rate : 1);
- return Number(value.toFixed(6));
- };
-
- export const formatDisplayCurrency = (
- usdAmount,
- currencyConfig,
- digits = 6,
- ) => {
- const symbol = currencyConfig?.symbol || '$';
- const value = formatDisplayCurrencyNumber(usdAmount, currencyConfig);
- return `${symbol}${value.toFixed(digits)}`;
- };
-
- export const pricingConfigToDisplayCurrency = (config, currencyConfig) => {
- const next = structuredClone(config || {});
- next.table = (next.table || []).map((row) => ({
- ...row,
- price: formatDisplayCurrencyNumber(row.price, currencyConfig),
- }));
- if (
- next.fallback?.strategy === FALLBACKS.default &&
- next.fallback.default_price !== undefined
- ) {
- next.fallback = {
- ...next.fallback,
- default_price: formatDisplayCurrencyNumber(
- next.fallback.default_price,
- currencyConfig,
- ),
- };
- }
- return next;
- };
-
- export const pricingConfigFromDisplayCurrency = (config, currencyConfig) => {
- const next = structuredClone(config || {});
- next.table = (next.table || []).map((row) => ({
- ...row,
- price: displayCurrencyToUSD(row.price, currencyConfig),
- }));
- if (
- next.fallback?.strategy === FALLBACKS.default &&
- next.fallback.default_price !== undefined
- ) {
- next.fallback = {
- ...next.fallback,
- default_price: displayCurrencyToUSD(
- next.fallback.default_price,
- currencyConfig,
- ),
- };
- }
- return next;
- };
-
- export const getBillingUnitLabel = (billingUnit, t = (key) => key) => {
- if (billingUnit === BILLING_UNITS.per1MTokens) return t('每 1M tokens');
- return t('每次调用');
- };
-
- export const getPriceUnitLabel = (
- billingUnit,
- symbol = '$',
- t = (key) => key,
- ) => `${symbol} / ${getBillingUnitLabel(billingUnit, t)}`;
-
- export const mergeGeneratedRows = ({
- existingRows,
- generatedRows,
- strategy,
- }) => {
- if (strategy === 'replace_all') return [...(generatedRows || [])];
- const manualRows = (existingRows || []).filter(
- (row) => row.source === ROW_SOURCES.manual,
- );
- return [...manualRows, ...(generatedRows || [])];
- };
-
- export const importPricingCsv = (text, dimensions) => {
- const lines = String(text || '')
- .trim()
- .split(/\r?\n/)
- .filter(Boolean);
- if (lines.length === 0) return { rows: [], errors: [] };
-
- const headers = parseCsvLine(lines[0]);
- const errors = [];
- const rows = lines.slice(1).map((line, index) => {
- const cells = parseCsvLine(line);
- const row = {};
- headers.forEach((header, i) => {
- row[header] = cells[i] ?? '';
- });
- for (const dim of dimensions || [])
- row[dim.key] =
- row[dim.key] === '*'
- ? '*'
- : parseDimensionValue(row[dim.key], dim.type);
- row.price = Number(row.price);
- row.source = row.source || ROW_SOURCES.manual;
- if (Number.isNaN(row.price))
- errors.push(`line ${index + 2} has invalid price`);
- return row;
- });
-
- return { rows, errors };
- };
-
- export const exportPricingCsv = (rows, dimensions) => {
- const headers = [
- ...(dimensions || []).map((dim) => dim.key),
- 'price',
- 'source',
- ];
- const body = (rows || []).map((row) =>
- headers.map((header) => escapeCsvValue(row[header] ?? '')).join(','),
- );
- return [headers.join(','), ...body].join('\n');
- };
-
- export const previewLookup = (config, requestValues) => {
- const validation = validatePricingConfig(config);
- if (!validation.ok)
- return { mode: 'invalid', error: validation.errors.join('\n') };
-
- const matches = [];
- for (const row of config.table || []) {
- let wildcards = 0;
- let matched = true;
- for (const dim of config.dimensions || []) {
- const expected = row[dim.key];
- if (expected === '*') {
- wildcards += 1;
- continue;
- }
- if (String(expected) !== String(requestValues?.[dim.key])) {
- matched = false;
- break;
- }
- }
- if (matched) matches.push({ row, wildcards });
- }
-
- matches.sort((a, b) => a.wildcards - b.wildcards);
- if (matches.length > 1 && matches[0].wildcards === matches[1].wildcards)
- return { mode: 'reject', error: 'ambiguous matching rows' };
- if (matches.length > 0) {
- const best = matches[0];
- return {
- mode: 'matched',
- row: best.row,
- price: Number(best.row.price),
- snapshot: buildSnapshot(
- config,
- requestValues,
- Number(best.row.price),
- 'matched',
- ),
- };
- }
-
- if (config.fallback.strategy === FALLBACKS.reject)
- return { mode: 'reject', error: 'no pricing row matched' };
- if (config.fallback.strategy === FALLBACKS.max) {
- const price = Math.max(
- ...config.table.map((row) => Number(row.price || 0)),
- );
- return {
- mode: 'fallback_max',
- price,
- snapshot: buildSnapshot(
- config,
- requestValues,
- price,
- 'fallback_max',
- FALLBACKS.max,
- ),
- };
- }
-
- const price = Number(config.fallback.default_price || 0);
- return {
- mode: 'fallback_default',
- price,
- snapshot: buildSnapshot(
- config,
- requestValues,
- price,
- 'fallback_default',
- FALLBACKS.default,
- ),
- };
- };
-
- export const parseDimensionValue = (value, type) => {
- if (value === '*') return value;
- if (type === 'number') return Number(value);
- if (type === 'boolean') return value === true || value === 'true';
- return value;
- };
-
- const valueMatchesType = (value, type) => {
- if (type === 'number')
- return typeof value === 'number' && !Number.isNaN(value);
- if (type === 'boolean') return typeof value === 'boolean';
- return typeof value === 'string';
- };
-
- const patternsOverlap = (left, right) =>
- left.every(
- (leftValue, index) =>
- leftValue === '*' ||
- right[index] === '*' ||
- String(leftValue) === String(right[index]),
- );
-
- const parseCsvLine = (line) => line.split(',').map((item) => item.trim());
-
- const escapeCsvValue = (value) => {
- const text = String(value);
- if (!/[",\n]/.test(text)) return text;
- return `"${text.replaceAll('"', '""')}"`;
- };
-
- const buildSnapshot = (config, values, price, mode, fallbackStrategy) => {
- const snapshot = {};
- for (const dim of config.dimensions || [])
- snapshot[dim.key] = values?.[dim.key] ?? null;
- snapshot.price = price;
- snapshot.billing_unit = config.billing_unit || BILLING_UNITS.perCall;
- snapshot.pricing_mode = mode;
- if (fallbackStrategy) snapshot.fallback_strategy = fallbackStrategy;
- return snapshot;
- };
|