From 00eba3e209802601505ffc2d6050a6151e869d06 Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Wed, 10 Jun 2026 14:27:42 +0800 Subject: [PATCH] Expose enterprise video pricing config --- package.json | 3 +- .../enterpriseVideoPricingContract.test.js | 54 +++++++++ src/enterpriseVideoBilling.js | 110 +++++++++++++----- src/routes/public.js | 7 +- 4 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 scripts/enterpriseVideoPricingContract.test.js diff --git a/package.json b/package.json index b0ffbcd..157d0ef 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,10 @@ "import-config": "node src/cli/importConfig.js", "init-pools": "node src/cli/initPools.js", "test:community-routes": "node scripts/communityRouteContract.test.js", + "test:enterprise-video-pricing": "node scripts/enterpriseVideoPricingContract.test.js", "test:key-manager": "node scripts/keyManagerReleaseContract.test.js", "test:provider-poll-limiter": "node scripts/providerPollLimiterContract.test.js", - "test": "npm run test:community-routes && npm run test:key-manager && npm run test:provider-poll-limiter" + "test": "npm run test:community-routes && npm run test:enterprise-video-pricing && npm run test:key-manager && npm run test:provider-poll-limiter" }, "dependencies": { "alipay-sdk": "^4.14.0", diff --git a/scripts/enterpriseVideoPricingContract.test.js b/scripts/enterpriseVideoPricingContract.test.js new file mode 100644 index 0000000..869a534 --- /dev/null +++ b/scripts/enterpriseVideoPricingContract.test.js @@ -0,0 +1,54 @@ +const assert = require("node:assert/strict"); + +const { + calculateEnterpriseVideoCredits, + getEnterpriseVideoCreditRate, + getEnterpriseVideoPricingConfig, +} = require("../src/enterpriseVideoBilling"); + +function getRule(config, id) { + const rule = config.rules.find((item) => item.id === id); + assert(rule, `missing enterprise video pricing rule: ${id}`); + return rule; +} + +const config = getEnterpriseVideoPricingConfig(); + +assert.equal(config.currency, "CNY"); +assert.equal(config.creditsPerCny, 100); +assert.equal(config.billingUnit, "per_second"); +assert.deepEqual(config.resolutions, ["720P", "1080P"]); + +assert.equal(getRule(config, "happyhorse").rates["720P"], 0.72); +assert.equal(getRule(config, "happyhorse").rates["1080P"], 1.28); +assert.equal(getRule(config, "wanxiang-i2v").rates["720P"], 0.6); +assert.equal(getRule(config, "kling-muted").rates["1080P"], 0.8); + +assert.equal( + getEnterpriseVideoCreditRate({ + model: "happyhorse-1.0", + resolution: "1080P", + }), + getRule(config, "happyhorse").rates["1080P"], +); + +assert.equal( + getEnterpriseVideoCreditRate({ + model: "kling-3.0-dashscope", + resolution: "720P", + muted: true, + hasReferenceVideo: false, + }), + getRule(config, "kling-muted").rates["720P"], +); + +assert.equal( + calculateEnterpriseVideoCredits({ + model: "vidu-q3-turbo", + resolution: "1080P", + durationSeconds: 5, + }), + 500, +); + +console.log("enterprise video pricing contract tests passed"); diff --git a/src/enterpriseVideoBilling.js b/src/enterpriseVideoBilling.js index 23dd027..d34ab06 100644 --- a/src/enterpriseVideoBilling.js +++ b/src/enterpriseVideoBilling.js @@ -21,6 +21,53 @@ const ENTERPRISE_VIDEO_ALLOWED_MODELS = new Set([ const CREDITS_PER_CNY = 100; const CREDIT_UNITS_PER_CREDIT = 100; +const ENTERPRISE_VIDEO_RESOLUTIONS = ["720P", "1080P"]; +const ENTERPRISE_VIDEO_DEFAULT_RESOLUTION = "1080P"; + +const ENTERPRISE_VIDEO_PRICING_RULES = [ + { + id: "happyhorse", + modelIncludes: ["happyhorse"], + rates: { "720P": 0.72, "1080P": 1.28 }, + }, + { + id: "wanxiang-i2v", + modelIncludes: ["wan2.7-i2v", "wanxiang"], + rates: { "720P": 0.6, "1080P": 1 }, + }, + { + id: "wan-animate-s2v", + modelIncludes: ["animate-mix", "s2v"], + rates: { "720P": 0.6, "1080P": 1 }, + }, + { + id: "kling-muted-reference", + modelIncludes: ["kling"], + when: { muted: true, hasReferenceVideo: true }, + rates: { "720P": 0.9, "1080P": 1.2 }, + }, + { + id: "kling-muted", + modelIncludes: ["kling"], + when: { muted: true, hasReferenceVideo: false }, + rates: { "720P": 0.6, "1080P": 0.8 }, + }, + { + id: "kling-default", + modelIncludes: ["kling"], + rates: { "720P": 0.9, "1080P": 1.2 }, + }, + { + id: "vidu", + modelIncludes: ["vidu"], + rates: { "720P": 0.6, "1080P": 1 }, + }, + { + id: "pixverse", + modelIncludes: ["pixverse"], + rates: { "720P": 0.6, "1080P": 1 }, + }, +]; function normalizeModel(value) { return String(value || "").trim().toLowerCase(); @@ -36,6 +83,21 @@ function normalizeEnterpriseVideoDuration(value) { return Math.max(1, Math.ceil(numeric)); } +function enterpriseVideoPricingRuleMatches(rule, input, model) { + if (!rule.modelIncludes.some((pattern) => model.includes(pattern))) return false; + if (!rule.when) return true; + if (Object.prototype.hasOwnProperty.call(rule.when, "muted") && Boolean(input.muted) !== rule.when.muted) { + return false; + } + if ( + Object.prototype.hasOwnProperty.call(rule.when, "hasReferenceVideo") && + Boolean(input.hasReferenceVideo) !== rule.when.hasReferenceVideo + ) { + return false; + } + return true; +} + function isEnterpriseVideoBillingUser(user) { return Boolean(user?.enterpriseId); } @@ -69,33 +131,10 @@ function getEnterpriseVideoCreditRate(input) { const resolution = normalizeEnterpriseVideoResolution(input.resolution || input.quality); const model = normalizeModel(input.model || input.requestedModel); - if (model.includes("happyhorse")) { - return resolution === "720P" ? 0.72 : 1.28; - } - - if (model.includes("wan2.7-i2v") || model.includes("wanxiang")) { - return resolution === "720P" ? 0.6 : 1; - } - - if (model.includes("animate-mix") || model.includes("s2v")) { - return resolution === "720P" ? 0.6 : 1; - } - - if (model.includes("kling")) { - if (input.muted) { - if (input.hasReferenceVideo) return resolution === "720P" ? 0.9 : 1.2; - return resolution === "720P" ? 0.6 : 0.8; - } - return resolution === "720P" ? 0.9 : 1.2; - } - - if (model.includes("vidu")) { - return resolution === "720P" ? 0.6 : 1.0; - } - - if (model.includes("pixverse")) { - return resolution === "720P" ? 0.6 : 1.0; - } + const rule = ENTERPRISE_VIDEO_PRICING_RULES.find((candidate) => + enterpriseVideoPricingRuleMatches(candidate, input, model), + ); + if (rule) return rule.rates[resolution] ?? rule.rates[ENTERPRISE_VIDEO_DEFAULT_RESOLUTION]; const error = new Error(`Unsupported enterprise video model: ${input.model || input.requestedModel}`); error.status = 403; @@ -103,6 +142,22 @@ function getEnterpriseVideoCreditRate(input) { throw error; } +function getEnterpriseVideoPricingConfig() { + return { + currency: "CNY", + creditsPerCny: CREDITS_PER_CNY, + billingUnit: "per_second", + defaultResolution: ENTERPRISE_VIDEO_DEFAULT_RESOLUTION, + resolutions: [...ENTERPRISE_VIDEO_RESOLUTIONS], + rules: ENTERPRISE_VIDEO_PRICING_RULES.map((rule) => ({ + id: rule.id, + modelIncludes: [...rule.modelIncludes], + when: rule.when ? { ...rule.when } : undefined, + rates: { ...rule.rates }, + })), + }; +} + function calculateEnterpriseVideoCredits(input) { const duration = normalizeEnterpriseVideoDuration(input.durationSeconds || input.duration); return Number((getEnterpriseVideoCreditRate(input) * duration * CREDITS_PER_CNY).toFixed(2)); @@ -233,6 +288,7 @@ module.exports = { assertEnterpriseVideoModelAllowed, calculateEnterpriseVideoCost, calculateEnterpriseVideoCredits, + getEnterpriseVideoPricingConfig, getEnterpriseVideoCreditRate, isEnterpriseVideoBillingUser, isEnterpriseVideoModelAllowed, diff --git a/src/routes/public.js b/src/routes/public.js index afcac4e..65d0c86 100644 --- a/src/routes/public.js +++ b/src/routes/public.js @@ -1,11 +1,16 @@ const { keyManager, listModelPrices, pool } = require("./context"); +const { getEnterpriseVideoPricingConfig } = require("../enterpriseVideoBilling"); function registerPriceRoutes(router) { // ── Public ─────────────────────────────────────────────────────────── router.get("/prices", async (_req, res) => { const prices = await listModelPrices({ enabledOnly: true }); - res.json(prices); + res.json({ + prices, + modelPrices: prices, + enterpriseVideoPricing: getEnterpriseVideoPricingConfig(), + }); }); }