export interface ModelPricingFloorTableConfig { field: string | null; mapping: Record; } export interface ModelPricingAddonConfig { field_key: string | null; operator: string; condition_value: unknown; condition_values: unknown[]; price: number | null; unit_field: string | null; } export interface ModelPricingMatrixRowConfig { price: number | null; price_cent: number | null; rate: number | null; price_per_unit: number | null; unit_price: number | null; raw: Record; } export interface ModelPricingConfig { mode: string | null; price_type: string | null; billing_mode: string | null; price: number | null; price_per_k: number | null; price_cent: number | null; price_per_k_cent: number | null; field: string | null; unit_field: string | null; unit_scale: number | null; round_digits: number | null; dimensions: string[]; rate_dimensions: string[]; matrix: ModelPricingMatrixRowConfig[]; conditional_addons: ModelPricingAddonConfig[]; floor_table: ModelPricingFloorTableConfig | null; submit_warning_threshold: number | null; raw: Record; } export interface ModelDefinition { id: string; name: string; pricing_config: ModelPricingConfig | null; } // ============================================================ // 工具函数 // ============================================================ /** 对应 Python: _as_float(value) -> Optional[float] */ function asFloat(value: unknown): number | null { if (value === null || value === undefined) return null; if (typeof value === "number" && !Number.isNaN(value)) return value; if (typeof value === "string") { const text = value.trim(); if (!text) return null; const parsed = Number(text); return Number.isNaN(parsed) ? null : parsed; } return null; } /** 对应 Python: _to_int(value, *, default=0) -> int */ function toInt(value: unknown, defaultValue: number = 0): number { if (value === null || value === undefined) return defaultValue; if (typeof value === "boolean") return value ? 1 : 0; if (typeof value === "number") return Math.trunc(value); if (typeof value === "string") { const text = value.trim(); if (!text) return defaultValue; const parsed = parseInt(text, 10); return Number.isNaN(parsed) ? defaultValue : parsed; } return defaultValue; } /** 对应 Python: _cent_to_yuan(value) -> float */ function centToYuan(value: unknown): number { return toInt(value, 0) / 100.0; } /** 对应 Python: _lookup_mapping_value(mapping, key, default="") -> object */ function lookupMappingValue( mapping: Record | null | undefined, key: unknown, defaultValue: unknown = "", ): unknown { const keyStr = String(key); if (mapping == null) return defaultValue; if (keyStr in mapping) return mapping[keyStr]; return defaultValue; } /** 对应 Python: _match_dimensions(entry, dimensions, inputs) -> bool */ function matchDimensions( entry: Record | null | undefined, dimensions: string[], inputs: Record, ): boolean { if (entry == null) return false; for (const dim of dimensions) { const entryValue = lookupMappingValue(entry, dim, null); if (entryValue === null || entryValue === undefined) continue; const inputVal = String(lookupMappingValue(inputs, dim, "")).trim().toLowerCase(); const entryVal = String(entryValue).trim().toLowerCase(); if (inputVal !== entryVal) return false; } return true; } /** * 从 matrix 行提取维度数据。 * * API 返回的 matrix 行中,维度字段(如 resolution、quality)在顶层, * 而非嵌套在 raw 子对象内。本函数合并 raw(如果存在)和行自身字段, * 兼容两种数据格式,确保 matchDimensions 能正确匹配。 */ function matrixRowData(row: ModelPricingMatrixRowConfig): Record { return { ...(row.raw ?? {}), ...row as unknown as Record }; } // ============================================================ // 金额格式化函数 // ============================================================ /** * 将元数值格式化为显示字符串,保留两位小数,去除尾部无意义的零和小数点。 * 对应 workbench.py / billing_dialog.py: _format_yuan(value: float) -> str */ export function formatYuan(value: number): string { const rounded = Math.round(value * 100) / 100; // .toFixed(2) → 去除尾部零 → 去除尾部小数点 return rounded.toFixed(2).replace(/\.?0+$/, ""); } /** * 将分(cent)转换为元并格式化。 * 对应 workbench.py / billing_dialog.py / account_dialog.py: _format_yuan_from_cent(cent: int) -> str */ export function formatYuanFromCent(cent: number): string { return formatYuan(cent / 100.0); } /** * 格式化金额(分→元),带符号和¥前缀。 * 对应 task_table.py: _format_money_cent(value: object) -> str */ export function formatMoneyCent(value: unknown): string { if (value === null || value === undefined) return "-"; if (typeof value === "boolean") return String(value); let cent: number; if (typeof value === "number") { cent = Math.trunc(value); } else if (typeof value === "string") { const parsed = parseInt(value, 10); if (Number.isNaN(parsed)) return value; cent = parsed; } else { return String(value); } const sign = cent < 0 ? "-" : ""; const centAbs = Math.abs(cent); const yuan = centAbs / 100.0; const text = yuan.toFixed(2).replace(/\.?0+$/, ""); return `${sign}¥${text}`; } /** * 格式化分→元,带符号和¥前缀。 * 对应 admin_console_dialog.py: _format_cent(cent: int) -> str */ export function formatCent(cent: number): string { const sign = cent < 0 ? "-" : ""; const valueAbs = Math.abs(Math.trunc(cent)); const yuan = valueAbs / 100.0; const text = yuan.toFixed(2).replace(/\.?0+$/, ""); return `${sign}¥${text}`; } // ============================================================ // 服务端计价逻辑 // ============================================================ /** 对应 Python: _calculate_server_simple(pricing, inputs) -> float */ function calculateServerSimple( pricing: ModelPricingConfig, inputs: Record, ): number { const priceCent = pricing.price_cent ?? 0; const unitField = (pricing.unit_field ?? "").trim(); if (!unitField) { return priceCent / 100.0; } const amount = asFloat(inputs[unitField]); const amountValue = amount === null ? 1.0 : amount; // (priceCent * amountValue) / 100 * 100 在 IEEE 754 下可能产生 // 浮点累积误差(例如 0.03*100→2.999...),直接 round 分子避免 return Math.round(priceCent * amountValue) / 100; } /** 对应 Python: _calculate_server_multi_dimension(pricing, inputs) -> Optional[float] */ function calculateServerMultiDimension( pricing: ModelPricingConfig, inputs: Record, ): number | null { const { dimensions, matrix } = pricing; if (!matrix || matrix.length === 0) return null; for (const row of matrix) { if (matchDimensions(matrixRowData(row), dimensions, inputs)) { return centToYuan(row.price_cent); } } const firstRow = matrix[0]; if (firstRow.price_cent !== null && firstRow.price_cent !== undefined) { return centToYuan(firstRow.price_cent); } return null; } /** 对应 Python: _calculate_server_billing_adapter(pricing, inputs) -> Optional[float] */ function calculateServerBillingAdapter( pricing: ModelPricingConfig, inputs: Record, ): number | null { const billingMode = (pricing.billing_mode ?? "rate").trim().toLowerCase(); let baseCostCent = 0; if (billingMode === "per_k_chars") { const field = (pricing.field ?? "prompt").trim() || "prompt"; const text = String(inputs[field] ?? ""); const pricePerKCent = pricing.price_per_k_cent ?? 0; baseCostCent = Math.floor((text.length + 999) / 1000) * pricePerKCent; } else { const matrix = pricing.matrix; if (!matrix || matrix.length === 0) return null; const dimensions = pricing.dimensions; const unitField = (pricing.unit_field ?? "duration").trim() || "duration"; const unitsRaw = asFloat(inputs[unitField]); let units = unitsRaw === null ? 1.0 : unitsRaw; // 底表修正 const floorTable = pricing.floor_table; if (floorTable) { const floorField = (floorTable.field ?? "").trim(); if (floorField === unitField) { const floorValue = asFloat(floorTable.mapping[String(Math.trunc(units))]); if (floorValue !== null && units < floorValue) { units = floorValue; } } } let rate: number | null = null; for (const row of matrix) { if (matchDimensions(matrixRowData(row), dimensions, inputs)) { rate = row.rate; break; } } if (rate === null) { rate = matrix[0].rate; } if (rate === null || rate === undefined) return null; baseCostCent = Math.round(rate * units * 100); } // 条件附加费 for (const addon of pricing.conditional_addons ?? []) { const fieldKey = (addon.field_key ?? "").trim(); const operator = addon.operator; let matched = false; if (operator === "PRESENT") { matched = Boolean(inputs[fieldKey]); } else if (operator === "EQ") { matched = String(inputs[fieldKey] ?? "") === String(addon.condition_value ?? ""); } else if (operator === "IN") { const conditionValues = (addon.condition_values ?? []).map((v) => String(v)); matched = conditionValues.includes(String(inputs[fieldKey] ?? "")); } if (!matched) continue; const addonPrice = addon.price; if (addonPrice === null || addonPrice === undefined) continue; const addonUnitField = String( addon.unit_field ?? pricing.unit_field ?? "duration", ).trim() || "duration"; const addonUnitsRaw = asFloat(inputs[addonUnitField]); const addonUnits = addonUnitsRaw === null ? 1.0 : addonUnitsRaw; baseCostCent += Math.round(addonPrice * addonUnits * 100); } return baseCostCent / 100.0; } // ============================================================ // 主计价入口 // ============================================================ /** * 根据模型定义和输入参数,动态计算消耗金额(单位:元)。 * 对应 Python: calculate_cost(model: ModelDefinition, inputs: Mapping) -> Optional[float] */ export function calculateCost( model: ModelDefinition, inputs: Record, ): number | null { const pricing = model.pricing_config; if (!pricing) return null; // ── 服务端定价 (price_type 存在且 mode 不存在) ── if (pricing.price_type && !pricing.mode) { const priceType = (pricing.price_type ?? "SIMPLE").trim().toUpperCase(); switch (priceType) { case "SIMPLE": return calculateServerSimple(pricing, inputs); case "MULTI_DIMENSION": return calculateServerMultiDimension(pricing, inputs); case "BILLING_ADAPTER": return calculateServerBillingAdapter(pricing, inputs); default: return null; } } // ── 客户端定价 ── const mode = pricing.mode; if (mode === "fixed") { if (pricing.price !== null && pricing.price !== undefined) { return pricing.price; } return Number(pricing.raw?.price ?? 0); } if (mode === "per_k_chars") { const fieldId = String(pricing.field ?? "text"); const rawText = inputs[fieldId] ?? ""; const text = rawText !== null && rawText !== undefined ? String(rawText) : ""; const pricePerK = pricing.price_per_k ?? Number(pricing.raw?.price_per_k ?? 0); return (text.length / 1000) * pricePerK; } if (mode === "rate_matrix") { const unitField = String(pricing.unit_field ?? "duration"); const unitRaw = inputs[unitField]; const unitValue = asFloat(unitRaw); if (unitValue === null) return null; const { rate_dimensions, matrix } = pricing; if (!rate_dimensions || rate_dimensions.length === 0 || !matrix || matrix.length === 0) { return null; } for (const entry of matrix) { if (matchDimensions(matrixRowData(entry), rate_dimensions, inputs)) { let rate = entry.rate; if (rate === null || rate === undefined) rate = entry.price_per_unit; if (rate === null || rate === undefined) rate = entry.unit_price; if (rate === null || rate === undefined) rate = 0.0; const rawRound = pricing.round_digits ?? pricing.raw?.round ?? 2; const roundDigits = typeof rawRound === "number" || (typeof rawRound === "string" && String(rawRound).trim()) ? Number(rawRound) : 2; return ( Math.round(unitValue * rate * Math.pow(10, roundDigits)) / Math.pow(10, roundDigits) ); } } return null; } if (mode === "matrix") { const { dimensions, matrix } = pricing; // 与 MULTI_DIMENSION 对齐:dimensions 为空时 matchDimensions 自动全匹配 if (!matrix || matrix.length === 0) { return null; } for (const entry of matrix) { if (matchDimensions(matrixRowData(entry), dimensions, inputs)) { // price_cent 优先:Python 端矩阵定价统一走 cent 分度值, // entry.price 可能为衍生/展示值,price_cent 才是计价基准 if (entry.price_cent !== null && entry.price_cent !== undefined) { return entry.price_cent / 100; } if (entry.price !== null && entry.price !== undefined) { return entry.price; } return Number(entry.raw?.price ?? 0); } } return null; } return null; } // ============================================================ // 定价依赖字段收集 // ============================================================ /** * 收集定价计算所依赖的输入字段名集合。 * 对应 Python: collect_pricing_dependent_fields(model: ModelDefinition) -> set[str] */ export function collectPricingDependentFields(model: ModelDefinition): Set { const pricing = model.pricing_config; if (!pricing) return new Set(); const fields = new Set(); const priceType = (pricing.price_type ?? "").trim().toUpperCase(); const mode = (pricing.mode ?? "").trim().toLowerCase(); const billingMode = (pricing.billing_mode ?? "").trim().toLowerCase(); if (priceType === "SIMPLE") { const unitField = (pricing.unit_field ?? "").trim(); if (unitField) fields.add(unitField); } else if (priceType === "MULTI_DIMENSION") { for (const dim of pricing.dimensions) { const dimText = String(dim).trim(); if (dimText) fields.add(dimText); } } else if (priceType === "BILLING_ADAPTER") { if (billingMode === "per_k_chars") { const field = String(pricing.field ?? "prompt").trim() || "prompt"; fields.add(field); } else { const unitField = String(pricing.unit_field ?? "duration").trim() || "duration"; fields.add(unitField); const floorTable = pricing.floor_table; if (floorTable) { const floorField = String(floorTable.field ?? "").trim(); if (floorField) fields.add(floorField); } for (const dim of pricing.dimensions) { const dimText = String(dim).trim(); if (dimText) fields.add(dimText); } } for (const addon of pricing.conditional_addons ?? []) { const fieldKey = String(addon.field_key ?? "").trim(); if (fieldKey) fields.add(fieldKey); const addonUnitField = String( addon.unit_field ?? pricing.unit_field ?? "duration", ).trim(); if (addonUnitField) fields.add(addonUnitField); } } else if (mode === "per_k_chars") { const field = String(pricing.field ?? "text").trim() || "text"; fields.add(field); } else if (mode === "rate_matrix") { const unitField = String(pricing.unit_field ?? "duration").trim() || "duration"; fields.add(unitField); for (const dim of pricing.rate_dimensions) { const dimText = String(dim).trim(); if (dimText) fields.add(dimText); } } else if (mode === "matrix") { for (const dim of pricing.dimensions) { const dimText = String(dim).trim(); if (dimText) fields.add(dimText); } } return fields; }