Fix duplicate session billing and align pricing with DeepSeek ×3.
Serialize billSessionUsage with row locks, expose rates on recharge, add compensation script and admin usage views. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+126
-35
@@ -912,55 +912,65 @@ export function createUserAuth(pool, options = {}) {
|
||||
};
|
||||
}
|
||||
const tokenState = normalizeTokenState(tokenStateRaw);
|
||||
const previous = await getBillingState(agentSessionId);
|
||||
if (
|
||||
previous &&
|
||||
tokenState.accumulatedInputTokens <= Number(previous.lastInputTokens ?? 0) &&
|
||||
tokenState.accumulatedOutputTokens <= Number(previous.lastOutputTokens ?? 0)
|
||||
) {
|
||||
const user = await getUserById(userId);
|
||||
return {
|
||||
ok: true,
|
||||
costCents: 0,
|
||||
balanceCents: user ? Number(user.balance_cents) : null,
|
||||
tokensUsed: user ? Number(user.tokens_used ?? 0) : null,
|
||||
deltaInputTokens: 0,
|
||||
deltaOutputTokens: 0,
|
||||
};
|
||||
}
|
||||
const config = loadBillingConfig();
|
||||
const costCents = computeDeltaCostCents(previous, tokenState, config);
|
||||
const deltaIn = Math.max(
|
||||
0,
|
||||
tokenState.accumulatedInputTokens - Number(previous?.lastInputTokens ?? 0),
|
||||
);
|
||||
const deltaOut = Math.max(
|
||||
0,
|
||||
tokenState.accumulatedOutputTokens - Number(previous?.lastOutputTokens ?? 0),
|
||||
);
|
||||
const deltaTokens = deltaIn + deltaOut;
|
||||
|
||||
const now = Date.now();
|
||||
const conn = await pool.getConnection();
|
||||
try {
|
||||
await conn.beginTransaction();
|
||||
|
||||
// Serialize concurrent Finish handlers for the same session.
|
||||
await conn.query(
|
||||
`INSERT INTO h5_session_billing_state
|
||||
(agent_session_id, user_id, last_accumulated_cost, last_input_tokens, last_output_tokens, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
last_accumulated_cost = VALUES(last_accumulated_cost),
|
||||
last_input_tokens = VALUES(last_input_tokens),
|
||||
last_output_tokens = VALUES(last_output_tokens),
|
||||
updated_at = VALUES(updated_at)`,
|
||||
VALUES (?, ?, NULL, 0, 0, ?)
|
||||
ON DUPLICATE KEY UPDATE agent_session_id = agent_session_id`,
|
||||
[agentSessionId, userId, now],
|
||||
);
|
||||
const [stateRows] = await conn.query(
|
||||
`SELECT last_accumulated_cost, last_input_tokens, last_output_tokens
|
||||
FROM h5_session_billing_state
|
||||
WHERE agent_session_id = ?
|
||||
FOR UPDATE`,
|
||||
[agentSessionId],
|
||||
);
|
||||
const stateRow = stateRows[0];
|
||||
const previous = {
|
||||
lastAccumulatedCost: stateRow?.last_accumulated_cost ?? null,
|
||||
lastInputTokens: Number(stateRow?.last_input_tokens ?? 0),
|
||||
lastOutputTokens: Number(stateRow?.last_output_tokens ?? 0),
|
||||
};
|
||||
|
||||
if (
|
||||
tokenState.accumulatedInputTokens <= previous.lastInputTokens &&
|
||||
tokenState.accumulatedOutputTokens <= previous.lastOutputTokens
|
||||
) {
|
||||
await conn.commit();
|
||||
const fresh = await getUserById(userId);
|
||||
return {
|
||||
ok: true,
|
||||
costCents: 0,
|
||||
balanceCents: fresh ? Number(fresh.balance_cents) : null,
|
||||
tokensUsed: fresh ? Number(fresh.tokens_used ?? 0) : null,
|
||||
deltaInputTokens: 0,
|
||||
deltaOutputTokens: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const costCents = computeDeltaCostCents(previous, tokenState, config);
|
||||
const deltaIn = Math.max(0, tokenState.accumulatedInputTokens - previous.lastInputTokens);
|
||||
const deltaOut = Math.max(0, tokenState.accumulatedOutputTokens - previous.lastOutputTokens);
|
||||
const deltaTokens = deltaIn + deltaOut;
|
||||
|
||||
await conn.query(
|
||||
`UPDATE h5_session_billing_state
|
||||
SET last_accumulated_cost = ?, last_input_tokens = ?, last_output_tokens = ?, updated_at = ?
|
||||
WHERE agent_session_id = ?`,
|
||||
[
|
||||
agentSessionId,
|
||||
userId,
|
||||
tokenState.accumulatedCost,
|
||||
tokenState.accumulatedInputTokens,
|
||||
tokenState.accumulatedOutputTokens,
|
||||
now,
|
||||
agentSessionId,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1039,6 +1049,86 @@ export function createUserAuth(pool, options = {}) {
|
||||
}
|
||||
};
|
||||
|
||||
const getUsageSummary = async ({ userId = null } = {}) => {
|
||||
const since24h = Date.now() - 24 * 60 * 60 * 1000;
|
||||
|
||||
if (userId) {
|
||||
const user = await getUserById(userId);
|
||||
if (!user) return null;
|
||||
|
||||
const [[usage24h]] = await pool.query(
|
||||
`SELECT COUNT(*) AS request_count,
|
||||
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
||||
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
||||
COALESCE(SUM(cost_cents), 0) AS cost_cents
|
||||
FROM h5_usage_records
|
||||
WHERE user_id = ? AND created_at >= ?`,
|
||||
[userId, since24h],
|
||||
);
|
||||
|
||||
const [[deductAll]] = await pool.query(
|
||||
`SELECT COUNT(*) AS request_count,
|
||||
COALESCE(SUM(ABS(amount_cents)), 0) AS cost_cents
|
||||
FROM h5_billing_ledger
|
||||
WHERE user_id = ? AND type = 'deduct'`,
|
||||
[userId],
|
||||
);
|
||||
|
||||
return {
|
||||
allTime: {
|
||||
requestCount: Number(deductAll.request_count),
|
||||
inputTokens: null,
|
||||
outputTokens: null,
|
||||
totalTokens: Number(user.tokens_used ?? 0),
|
||||
costCents: Number(user.spent_cents ?? deductAll.cost_cents),
|
||||
},
|
||||
last24h: {
|
||||
requestCount: Number(usage24h.request_count),
|
||||
inputTokens: Number(usage24h.input_tokens),
|
||||
outputTokens: Number(usage24h.output_tokens),
|
||||
totalTokens:
|
||||
Number(usage24h.input_tokens) + Number(usage24h.output_tokens),
|
||||
costCents: Number(usage24h.cost_cents),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const [[wallet]] = await pool.query(
|
||||
`SELECT COALESCE(SUM(tokens_used), 0) AS total_tokens FROM h5_user_wallets`,
|
||||
);
|
||||
const [[deductAll]] = await pool.query(
|
||||
`SELECT COUNT(*) AS request_count,
|
||||
COALESCE(SUM(ABS(amount_cents)), 0) AS cost_cents
|
||||
FROM h5_billing_ledger
|
||||
WHERE type = 'deduct'`,
|
||||
);
|
||||
const [[deduct24h]] = await pool.query(
|
||||
`SELECT COUNT(*) AS request_count,
|
||||
COALESCE(SUM(ABS(amount_cents)), 0) AS cost_cents,
|
||||
COALESCE(SUM(tokens), 0) AS total_tokens
|
||||
FROM h5_billing_ledger
|
||||
WHERE type = 'deduct' AND created_at >= ?`,
|
||||
[since24h],
|
||||
);
|
||||
|
||||
return {
|
||||
allTime: {
|
||||
requestCount: Number(deductAll.request_count),
|
||||
inputTokens: null,
|
||||
outputTokens: null,
|
||||
totalTokens: Number(wallet.total_tokens),
|
||||
costCents: Number(deductAll.cost_cents),
|
||||
},
|
||||
last24h: {
|
||||
requestCount: Number(deduct24h.request_count),
|
||||
inputTokens: null,
|
||||
outputTokens: null,
|
||||
totalTokens: Number(deduct24h.total_tokens),
|
||||
costCents: Number(deduct24h.cost_cents),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const listUsageRecords = async ({ userId = null, limit = 50 } = {}) => {
|
||||
const safeLimit = Math.min(Math.max(Number(limit) || 50, 1), 200);
|
||||
const params = [];
|
||||
@@ -2151,6 +2241,7 @@ export function createUserAuth(pool, options = {}) {
|
||||
recharge,
|
||||
billSessionUsage,
|
||||
listUsageRecords,
|
||||
getUsageSummary,
|
||||
listBillingLedger,
|
||||
getAdminSummary,
|
||||
ensureAdminUser,
|
||||
|
||||
Reference in New Issue
Block a user