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; };