diff --git a/.env.example b/.env.example index 26fce87..7ce631e 100644 --- a/.env.example +++ b/.env.example @@ -280,6 +280,9 @@ H5_ACCESS_PASSWORD=change-me # H5_COST_ESTIMATE_FROM_TOKENS=1 # H5_COST_ESTIMATE_INPUT_USD_PER_1M=0.27 # H5_COST_ESTIMATE_OUTPUT_USD_PER_1M=1.1 +# 上述计费公式也可在 memind_adm「计费中心 → 计量公式」后台覆盖(写入 h5_billing_admin_config)。 +# 设为 env 时强制只读环境变量,禁止后台改写: +# H5_BILLING_CONFIG_SOURCE=env # 用户自助充值(微信支付) # H5_RECHARGE_TIERS_CENTS=500,1000,3000,5000,10000,20000 diff --git a/billing-admin-config.mjs b/billing-admin-config.mjs new file mode 100644 index 0000000..9a2c4cc --- /dev/null +++ b/billing-admin-config.mjs @@ -0,0 +1,310 @@ +import { loadBillingConfig } from './billing.mjs'; + +const CONFIG_TABLE = 'h5_billing_admin_config'; +const CONFIG_SCOPE = 'global'; +const SOURCE_ADMIN_DB = 'admin-db'; +const SOURCE_ENV = 'env'; +const SOURCE_ENV_OVERRIDE = 'env-override'; +const SOURCE_DEFAULT = 'default'; +const CACHE_TTL_MS = 5_000; +const DEFAULT_ESTIMATE_INPUT_USD_PER_1M = 0.27; +const DEFAULT_ESTIMATE_OUTPUT_USD_PER_1M = 1.1; + +function normalizeBoolean(value, fallback = false) { + if (value == null || value === '') return fallback; + if (typeof value === 'boolean') return value; + const normalized = String(value).trim().toLowerCase(); + if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; + if (['0', 'false', 'no', 'off'].includes(normalized)) return false; + return fallback; +} + +function normalizePositiveNumber(value, fallback) { + const num = Number(value); + if (!Number.isFinite(num) || num <= 0) return fallback; + return num; +} + +function normalizeNonNegativeNumber(value, fallback) { + const num = Number(value); + if (!Number.isFinite(num) || num < 0) return fallback; + return num; +} + +function defaultConfigShape(env = process.env) { + const billing = loadBillingConfig(env); + const costEstimateFromTokens = env.H5_COST_ESTIMATE_FROM_TOKENS !== '0'; + const inputUsdPer1M = Number(env.H5_COST_ESTIMATE_INPUT_USD_PER_1M ?? DEFAULT_ESTIMATE_INPUT_USD_PER_1M); + const outputUsdPer1M = Number(env.H5_COST_ESTIMATE_OUTPUT_USD_PER_1M ?? DEFAULT_ESTIMATE_OUTPUT_USD_PER_1M); + return { + useBackendCost: billing.useBackendCost, + usdCnyRate: billing.usdCnyRate, + marginMultiplier: billing.marginMultiplier, + inputCentsPer1k: billing.inputCentsPer1k, + outputCentsPer1k: billing.outputCentsPer1k, + minBillCents: billing.minBillCents, + costEstimateFromTokens, + costEstimateInputUsdPer1M: + Number.isFinite(inputUsdPer1M) && inputUsdPer1M >= 0 + ? inputUsdPer1M + : DEFAULT_ESTIMATE_INPUT_USD_PER_1M, + costEstimateOutputUsdPer1M: + Number.isFinite(outputUsdPer1M) && outputUsdPer1M >= 0 + ? outputUsdPer1M + : DEFAULT_ESTIMATE_OUTPUT_USD_PER_1M, + }; +} + +function cloneConfig(config = null, env = process.env) { + return structuredClone?.(config ?? defaultConfigShape(env)) + ?? JSON.parse(JSON.stringify(config ?? defaultConfigShape(env))); +} + +function parseJsonLike(value, fallback) { + if (value == null || value === '') return fallback; + if (typeof value === 'string') { + try { + return JSON.parse(value); + } catch { + return fallback; + } + } + if (typeof value === 'object') return value; + return fallback; +} + +function envLocked(env = process.env) { + return String(env.H5_BILLING_CONFIG_SOURCE ?? '').trim().toLowerCase() === 'env'; +} + +function mergePatch(currentConfig, patch = {}, env = process.env) { + const base = defaultConfigShape(env); + const next = cloneConfig({ ...base, ...currentConfig }, env); + const raw = patch?.config && typeof patch.config === 'object' ? patch.config : patch; + + if ('useBackendCost' in raw) next.useBackendCost = normalizeBoolean(raw.useBackendCost, next.useBackendCost); + if ('usdCnyRate' in raw) next.usdCnyRate = normalizePositiveNumber(raw.usdCnyRate, next.usdCnyRate); + if ('marginMultiplier' in raw) { + next.marginMultiplier = normalizePositiveNumber(raw.marginMultiplier, next.marginMultiplier); + } + if ('inputCentsPer1k' in raw) { + next.inputCentsPer1k = normalizeNonNegativeNumber(raw.inputCentsPer1k, next.inputCentsPer1k); + } + if ('outputCentsPer1k' in raw) { + next.outputCentsPer1k = normalizeNonNegativeNumber(raw.outputCentsPer1k, next.outputCentsPer1k); + } + if ('minBillCents' in raw) { + next.minBillCents = normalizePositiveNumber(raw.minBillCents, next.minBillCents); + } + if ('costEstimateFromTokens' in raw) { + next.costEstimateFromTokens = normalizeBoolean(raw.costEstimateFromTokens, next.costEstimateFromTokens); + } + if ('costEstimateInputUsdPer1M' in raw) { + next.costEstimateInputUsdPer1M = normalizeNonNegativeNumber( + raw.costEstimateInputUsdPer1M, + next.costEstimateInputUsdPer1M, + ); + } + if ('costEstimateOutputUsdPer1M' in raw) { + next.costEstimateOutputUsdPer1M = normalizeNonNegativeNumber( + raw.costEstimateOutputUsdPer1M, + next.costEstimateOutputUsdPer1M, + ); + } + return next; +} + +export function toComputeBillingConfig(config) { + return { + useBackendCost: Boolean(config?.useBackendCost), + usdCnyRate: Number(config?.usdCnyRate ?? 7.2), + marginMultiplier: Number(config?.marginMultiplier ?? 1), + inputCentsPer1k: Number(config?.inputCentsPer1k ?? 2), + outputCentsPer1k: Number(config?.outputCentsPer1k ?? 6), + minBillCents: Number(config?.minBillCents ?? 1), + }; +} + +export function toCostEstimateConfig(config) { + const useBackendCost = Boolean(config?.useBackendCost); + const enabled = useBackendCost && config?.costEstimateFromTokens !== false; + return { + enabled, + inputUsdPer1M: Number(config?.costEstimateInputUsdPer1M ?? 0.27), + outputUsdPer1M: Number(config?.costEstimateOutputUsdPer1M ?? 1.1), + }; +} + +function envLooksCustomized(env = process.env) { + return [ + 'H5_USE_BACKEND_COST', + 'H5_USD_CNY_RATE', + 'H5_MARGIN_MULTIPLIER', + 'H5_BILL_INPUT_CENTS_PER_1K', + 'H5_BILL_OUTPUT_CENTS_PER_1K', + 'H5_MIN_BILL_CENTS', + 'H5_COST_ESTIMATE_FROM_TOKENS', + 'H5_COST_ESTIMATE_INPUT_USD_PER_1M', + 'H5_COST_ESTIMATE_OUTPUT_USD_PER_1M', + ].some((key) => String(env[key] ?? '').trim() !== ''); +} + +async function ensureConfigTable(pool) { + await pool.query(` + CREATE TABLE IF NOT EXISTS ${CONFIG_TABLE} ( + config_scope VARCHAR(32) PRIMARY KEY, + config_json JSON NOT NULL, + updated_by CHAR(36) NULL, + updated_at BIGINT NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); +} + +async function loadStoredState(pool) { + await ensureConfigTable(pool); + const [rows] = await pool.query( + `SELECT config_json, updated_by, updated_at + FROM ${CONFIG_TABLE} + WHERE config_scope = ? + LIMIT 1`, + [CONFIG_SCOPE], + ); + const row = rows[0]; + if (!row) return null; + return { + config: mergePatch(defaultConfigShape(), parseJsonLike(row.config_json, {})), + updatedAt: Number(row.updated_at ?? 0) || null, + updatedBy: row.updated_by ?? null, + }; +} + +export function createBillingAdminConfigService(pool, { env = process.env, cacheTtlMs = CACHE_TTL_MS } = {}) { + let cache = null; + + function clearCache() { + cache = null; + } + + async function loadEffectiveConfig({ bypassCache = false } = {}) { + const now = Date.now(); + if (!bypassCache && cache && now - cache.loadedAt < cacheTtlMs) { + return cache.state; + } + + if (envLocked(env)) { + const state = { + config: defaultConfigShape(env), + updatedAt: null, + updatedBy: null, + source: SOURCE_ENV_OVERRIDE, + }; + cache = { loadedAt: now, state }; + return state; + } + + const stored = await loadStoredState(pool); + let state; + if (stored) { + state = { + config: cloneConfig(stored.config, env), + updatedAt: stored.updatedAt, + updatedBy: stored.updatedBy, + source: SOURCE_ADMIN_DB, + }; + } else if (envLooksCustomized(env)) { + state = { + config: defaultConfigShape(env), + updatedAt: null, + updatedBy: null, + source: SOURCE_ENV, + }; + } else { + state = { + config: defaultConfigShape(env), + updatedAt: null, + updatedBy: null, + source: SOURCE_DEFAULT, + }; + } + cache = { loadedAt: now, state }; + return state; + } + + return { + async ensureSchema() { + await ensureConfigTable(pool); + }, + + clearCache, + + async getAdminConfig() { + const state = await loadEffectiveConfig({ bypassCache: true }); + return { + config: state.config, + updatedAt: state.updatedAt, + updatedBy: state.updatedBy, + source: state.source, + envOverrideActive: state.source === SOURCE_ENV_OVERRIDE, + formula: '最终扣费 = 上游成本(USD) × 汇率 × 毛利倍数(成本模式);无上游成本时回退 Token 单价', + }; + }, + + async updateAdminConfig(patch = {}, { updatedBy = null } = {}) { + if (envLocked(env)) { + throw Object.assign(new Error('H5_BILLING_CONFIG_SOURCE=env 时不允许通过后台修改'), { + code: 'BILLING_CONFIG_ENV_LOCKED', + }); + } + const stored = await loadStoredState(pool); + const base = stored?.config ?? defaultConfigShape(env); + const nextConfig = mergePatch(base, patch, env); + await ensureConfigTable(pool); + const now = Date.now(); + await pool.query( + `INSERT INTO ${CONFIG_TABLE} + (config_scope, config_json, updated_by, updated_at) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + config_json = VALUES(config_json), + updated_by = VALUES(updated_by), + updated_at = VALUES(updated_at)`, + [CONFIG_SCOPE, JSON.stringify(nextConfig), updatedBy, now], + ); + clearCache(); + return this.getAdminConfig(); + }, + + async getRuntimeState() { + const state = await loadEffectiveConfig(); + return { + source: state.source, + updatedAt: state.updatedAt, + updatedBy: state.updatedBy, + config: state.config, + compute: toComputeBillingConfig(state.config), + estimate: toCostEstimateConfig(state.config), + envOverrideActive: state.source === SOURCE_ENV_OVERRIDE, + }; + }, + + async getEffectiveBillingConfig() { + const state = await loadEffectiveConfig(); + return toComputeBillingConfig(state.config); + }, + + async getEffectiveCostEstimateConfig() { + const state = await loadEffectiveConfig(); + return toCostEstimateConfig(state.config); + }, + }; +} + +export const billingAdminConfigInternals = { + CONFIG_SCOPE, + CONFIG_TABLE, + defaultConfigShape, + mergePatch, + toComputeBillingConfig, + toCostEstimateConfig, + envLocked, +}; diff --git a/billing-admin-config.test.mjs b/billing-admin-config.test.mjs new file mode 100644 index 0000000..a8ad9e4 --- /dev/null +++ b/billing-admin-config.test.mjs @@ -0,0 +1,131 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { + billingAdminConfigInternals, + createBillingAdminConfigService, + toComputeBillingConfig, + toCostEstimateConfig, +} from './billing-admin-config.mjs'; + +function createMemoryPool(initialRows = []) { + const rows = new Map( + initialRows.map((row) => [row.config_scope, { ...row }]), + ); + return { + async query(sql, params = []) { + if (/CREATE TABLE/i.test(sql)) return [{}, undefined]; + if (/SELECT/i.test(sql)) { + const scope = params[0]; + const row = rows.get(scope); + return [row ? [row] : [], undefined]; + } + if (/INSERT INTO/i.test(sql)) { + const [scope, configJson, updatedBy, updatedAt] = params; + rows.set(scope, { + config_scope: scope, + config_json: configJson, + updated_by: updatedBy, + updated_at: updatedAt, + }); + return [{ affectedRows: 1 }, undefined]; + } + return [{}, undefined]; + }, + _rows: rows, + }; +} + +test('mergePatch validates margin and FX', () => { + const next = billingAdminConfigInternals.mergePatch( + billingAdminConfigInternals.defaultConfigShape({}), + { + marginMultiplier: 1.5, + usdCnyRate: 7.1, + useBackendCost: true, + }, + {}, + ); + assert.equal(next.marginMultiplier, 1.5); + assert.equal(next.usdCnyRate, 7.1); + assert.equal(next.useBackendCost, true); +}); + +test('toComputeBillingConfig maps admin shape', () => { + const compute = toComputeBillingConfig({ + useBackendCost: true, + usdCnyRate: 7.2, + marginMultiplier: 1.2, + inputCentsPer1k: 2, + outputCentsPer1k: 6, + minBillCents: 1, + }); + assert.deepEqual(compute, { + useBackendCost: true, + usdCnyRate: 7.2, + marginMultiplier: 1.2, + inputCentsPer1k: 2, + outputCentsPer1k: 6, + minBillCents: 1, + }); +}); + +test('toCostEstimateConfig disables when cost mode off', () => { + const estimate = toCostEstimateConfig({ + useBackendCost: false, + costEstimateFromTokens: true, + costEstimateInputUsdPer1M: 0.27, + costEstimateOutputUsdPer1M: 1.1, + }); + assert.equal(estimate.enabled, false); +}); + +test('admin-db config wins over env for effective billing', async () => { + const pool = createMemoryPool(); + const env = { + H5_USE_BACKEND_COST: '1', + H5_USD_CNY_RATE: '7.2', + H5_MARGIN_MULTIPLIER: '1.2', + }; + const service = createBillingAdminConfigService(pool, { env, cacheTtlMs: 0 }); + await service.updateAdminConfig( + { marginMultiplier: 2, usdCnyRate: 7.5, useBackendCost: true }, + { updatedBy: 'admin-1' }, + ); + const effective = await service.getEffectiveBillingConfig(); + assert.equal(effective.marginMultiplier, 2); + assert.equal(effective.usdCnyRate, 7.5); + const admin = await service.getAdminConfig(); + assert.equal(admin.source, 'admin-db'); + assert.equal(admin.updatedBy, 'admin-1'); +}); + +test('H5_BILLING_CONFIG_SOURCE=env locks admin writes', async () => { + const pool = createMemoryPool(); + const service = createBillingAdminConfigService(pool, { + env: { H5_BILLING_CONFIG_SOURCE: 'env', H5_MARGIN_MULTIPLIER: '1.2' }, + cacheTtlMs: 0, + }); + await assert.rejects( + () => service.updateAdminConfig({ marginMultiplier: 9 }), + /H5_BILLING_CONFIG_SOURCE=env/, + ); + const admin = await service.getAdminConfig(); + assert.equal(admin.source, 'env-override'); + assert.equal(admin.config.marginMultiplier, 1.2); +}); + +test('env fallback used when no admin row', async () => { + const pool = createMemoryPool(); + const service = createBillingAdminConfigService(pool, { + env: { + H5_USE_BACKEND_COST: '1', + H5_MARGIN_MULTIPLIER: '1.2', + H5_USD_CNY_RATE: '7.2', + }, + cacheTtlMs: 0, + }); + const admin = await service.getAdminConfig(); + assert.equal(admin.source, 'env'); + assert.equal(admin.config.marginMultiplier, 1.2); + assert.equal(admin.config.useBackendCost, true); +}); diff --git a/billing-token-state.mjs b/billing-token-state.mjs index e5d6610..c135cd7 100644 --- a/billing-token-state.mjs +++ b/billing-token-state.mjs @@ -35,7 +35,7 @@ export function estimateAccumulatedCostUsd(tokenStateRaw, estimateConfig = loadC export function enrichTokenStateForBilling( tokenStateRaw, - { sessionCost = null } = {}, + { sessionCost = null, estimateConfig = null } = {}, env = process.env, ) { const state = normalizeTokenState(tokenStateRaw); @@ -49,7 +49,10 @@ export function enrichTokenStateForBilling( return state; } - const estimatedUsd = estimateAccumulatedCostUsd(state, loadCostEstimateConfig(env)); + const estimatedUsd = estimateAccumulatedCostUsd( + state, + estimateConfig ?? loadCostEstimateConfig(env), + ); if (estimatedUsd != null) { return { ...state, accumulatedCost: estimatedUsd }; } @@ -86,7 +89,13 @@ async function resolveSessionCostPayload(sessionId, fetchSession, fetchSessionCo export async function resolveBillingTokenState( tokenStateRaw, - { sessionId = null, fetchSession = null, fetchSessionCostFromPg = null } = {}, + { + sessionId = null, + fetchSession = null, + fetchSessionCostFromPg = null, + estimateConfig = null, + loadEstimateConfig = null, + } = {}, env = process.env, ) { const state = normalizeTokenState(tokenStateRaw); @@ -96,5 +105,9 @@ export async function resolveBillingTokenState( fetchSessionCostFromPg, env, ); - return enrichTokenStateForBilling(state, { sessionCost }, env); + const resolvedEstimateConfig = + estimateConfig + ?? (typeof loadEstimateConfig === 'function' ? await loadEstimateConfig() : null) + ?? loadCostEstimateConfig(env); + return enrichTokenStateForBilling(state, { sessionCost, estimateConfig: resolvedEstimateConfig }, env); } diff --git a/billing.mjs b/billing.mjs index a04ee8b..fc4b50c 100644 --- a/billing.mjs +++ b/billing.mjs @@ -1,16 +1,17 @@ -export function loadBillingConfig() { +export function loadBillingConfig(env = process.env) { // 默认按人民币分(CNY cents)计费;仅当 H5_USE_BACKEND_COST=1 时才用上游 USD 成本换算。 - const useBackendCost = process.env.H5_USE_BACKEND_COST === '1'; + const useBackendCost = env.H5_USE_BACKEND_COST === '1'; // 成本模式下的毛利倍数:最终扣费 = 上游真实成本(USD) × 汇率 × marginMultiplier。 // 默认 1(按成本价卖,零毛利)——启用 useBackendCost 时务必显式设置目标倍数。 - const marginMultiplier = Number(process.env.H5_MARGIN_MULTIPLIER ?? 1); + // 生产可通过 memind_adm「计费中心 → 计量公式」覆盖(见 billing-admin-config.mjs)。 + const marginMultiplier = Number(env.H5_MARGIN_MULTIPLIER ?? 1); return { useBackendCost, - usdCnyRate: Number(process.env.H5_USD_CNY_RATE ?? 7.2), + usdCnyRate: Number(env.H5_USD_CNY_RATE ?? 7.2), marginMultiplier: Number.isFinite(marginMultiplier) && marginMultiplier > 0 ? marginMultiplier : 1, - inputCentsPer1k: Number(process.env.H5_BILL_INPUT_CENTS_PER_1K ?? 2), - outputCentsPer1k: Number(process.env.H5_BILL_OUTPUT_CENTS_PER_1K ?? 6), - minBillCents: Number(process.env.H5_MIN_BILL_CENTS ?? 1), + inputCentsPer1k: Number(env.H5_BILL_INPUT_CENTS_PER_1K ?? 2), + outputCentsPer1k: Number(env.H5_BILL_OUTPUT_CENTS_PER_1K ?? 6), + minBillCents: Number(env.H5_MIN_BILL_CENTS ?? 1), }; } diff --git a/docs/h5-metering-gateway.md b/docs/h5-metering-gateway.md index b839a6e..6619bbe 100644 --- a/docs/h5-metering-gateway.md +++ b/docs/h5-metering-gateway.md @@ -20,6 +20,7 @@ - 仅当上游回传 `accumulatedCost` 时生效;缺失则**回退**原 flat token 路径,不破坏现有计费。 - 若 goose Finish / session 均未带 cost,且 `H5_COST_ESTIMATE_FROM_TOKENS=1`(成本模式默认开启),Portal 会按 DeepSeek 中继粗估价(`billing-token-state.mjs`)补齐 `accumulatedCost`,再走 `× margin`。 - 生产启用:`.env` 设 `H5_USE_BACKEND_COST=1` + `H5_MARGIN_MULTIPLIER=<目标毛利>`。 +- 也可在 memind_adm「计费中心 → 计量公式」修改同一套参数(`h5_billing_admin_config`,优先级高于 env;`H5_BILLING_CONFIG_SOURCE=env` 可锁定)。 > ⚠️ 上线前需确认:一帧真实 SSE 的 `token_state` 是否带 `accumulated_cost`(goose `sessions.db` 有该列,但要确认 SSE Finish 事件也序列化了它)。确认前 multiplier 改动是安全的(无 cost 即回退)。 diff --git a/server.mjs b/server.mjs index 86d7f1c..4d8afb1 100644 --- a/server.mjs +++ b/server.mjs @@ -412,6 +412,8 @@ async function bootstrapUserAuth() { }); subscriptionService = authServices.subscriptionService; + const billingConfigService = + authServices.billingConfigService; userAuth = authServices.userAuth; sessionAccess = authServices.sessionAccess; wechatPayClient = authServices.wechatPayClient; @@ -501,6 +503,7 @@ async function bootstrapUserAuth() { sessionStreamStore, llmProviderService, subscriptionService, + billingConfigService, sessionSnapshotService, conversationMemoryService, memoryV2, diff --git a/server/portal-auth-services-bootstrap.mjs b/server/portal-auth-services-bootstrap.mjs index 809fb5a..937a37e 100644 --- a/server/portal-auth-services-bootstrap.mjs +++ b/server/portal-auth-services-bootstrap.mjs @@ -1,3 +1,4 @@ +import { createBillingAdminConfigService } from '../billing-admin-config.mjs'; import { createRechargeService } from '../billing-recharge.mjs'; import { createPlanCatalogService, @@ -53,6 +54,7 @@ export async function bootstrapPortalAuthServices({ loadWechatOAuthConfigFn = loadWechatOAuthConfig, createWechatOAuthServiceFn = createWechatOAuthService, createRechargeServiceFn = createRechargeService, + createBillingAdminConfigServiceFn = createBillingAdminConfigService, } = {}) { if ( !pool || @@ -77,6 +79,10 @@ export async function bootstrapPortalAuthServices({ subscriptionService._planCatalogService = planCatalogService; + const billingConfigService = + createBillingAdminConfigServiceFn(pool, { env }); + await billingConfigService.ensureSchema(); + const userAuth = createUserAuthFn(pool, { usersRoot, h5Root, @@ -84,6 +90,7 @@ export async function bootstrapPortalAuthServices({ env.H5_SIGNUP_BALANCE_CENTS ?? 500, ), subscriptionService, + billingConfigService, getMindSearchConfig: () => mindSearchConfigService.getEffectiveConfig(), provisionUserDataSpace: async ({ @@ -158,6 +165,7 @@ export async function bootstrapPortalAuthServices({ return { planCatalogService, subscriptionService, + billingConfigService, userAuth, sessionAccess, routerDecisionMode, diff --git a/server/portal-gateway-services-bootstrap.mjs b/server/portal-gateway-services-bootstrap.mjs index 2f2ac1f..9c52d5d 100644 --- a/server/portal-gateway-services-bootstrap.mjs +++ b/server/portal-gateway-services-bootstrap.mjs @@ -129,6 +129,7 @@ export function bootstrapPortalGatewayServices({ sessionStreamStore, llmProviderService, subscriptionService, + billingConfigService = null, sessionSnapshotService, conversationMemoryService, memoryV2, @@ -176,6 +177,7 @@ export function bootstrapPortalGatewayServices({ sessionStreamStore, llmProviderService, subscriptionService, + billingConfigService, sessionSnapshotService, conversationMemoryService, memoryV2, diff --git a/tkmind-proxy.mjs b/tkmind-proxy.mjs index 4974f35..93dad49 100644 --- a/tkmind-proxy.mjs +++ b/tkmind-proxy.mjs @@ -1138,6 +1138,7 @@ export function createTkmindProxy({ llmProviderService, localFetchAsset, subscriptionService, + billingConfigService = null, sessionSnapshotService, conversationMemoryService, memoryV2, @@ -2097,6 +2098,9 @@ export function createTkmindProxy({ return resolveBillingTokenState(tokenStateRaw, { sessionId, fetchSession: fetchSessionBillingCost, + loadEstimateConfig: billingConfigService?.getEffectiveCostEstimateConfig + ? () => billingConfigService.getEffectiveCostEstimateConfig() + : null, }); } diff --git a/user-auth.mjs b/user-auth.mjs index 3504c0a..8639b2c 100644 --- a/user-auth.mjs +++ b/user-auth.mjs @@ -142,6 +142,7 @@ export function createUserAuth(pool, options = {}) { ? options.provisionUserDataSpace : null; const getMindSearchConfig = typeof options.getMindSearchConfig === 'function' ? options.getMindSearchConfig : null; + const billingConfigService = options.billingConfigService ?? null; const sessions = new Map(); const loginFailures = new Map(); @@ -1334,7 +1335,9 @@ export function createUserAuth(pool, options = {}) { }; } const tokenState = normalizeTokenState(tokenStateRaw); - const config = loadBillingConfig(); + const config = billingConfigService?.getEffectiveBillingConfig + ? await billingConfigService.getEffectiveBillingConfig() + : loadBillingConfig(env); const normalizedRequestId = requestId ? String(requestId).trim() || null : null; const now = Date.now(); const conn = await pool.getConnection();