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:
John
2026-06-16 03:10:05 -07:00
parent befeedfd37
commit 03690ee354
14 changed files with 472 additions and 52 deletions
+126 -35
View File
@@ -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,