fix(billing): read Goose accumulated_cost from PostgreSQL fallback
Goosed GET /sessions/:id returns accumulated_cost null while memind_sessions PG has the real upstream cost. Fall back to PG before token estimate billing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+36
-13
@@ -1,4 +1,5 @@
|
||||
import { normalizeTokenState } from './billing.mjs';
|
||||
import { fetchGooseSessionAccumulatedCostUsd } from './goose-session-cost.mjs';
|
||||
|
||||
export function loadCostEstimateConfig(env = process.env) {
|
||||
const useBackendCost = env.H5_USE_BACKEND_COST === '1';
|
||||
@@ -56,22 +57,44 @@ export function enrichTokenStateForBilling(
|
||||
return state;
|
||||
}
|
||||
|
||||
async function resolveSessionCostPayload(sessionId, fetchSession, fetchSessionCostFromPg, env) {
|
||||
let sessionCost = null;
|
||||
if (typeof fetchSession === 'function' && sessionId) {
|
||||
try {
|
||||
sessionCost = await fetchSession(sessionId);
|
||||
} catch {
|
||||
sessionCost = null;
|
||||
}
|
||||
}
|
||||
if (pickSessionAccumulatedCost(sessionCost) != null) {
|
||||
return sessionCost;
|
||||
}
|
||||
|
||||
const readPgCost =
|
||||
typeof fetchSessionCostFromPg === 'function'
|
||||
? fetchSessionCostFromPg
|
||||
: (sid) => fetchGooseSessionAccumulatedCostUsd(sid, env);
|
||||
const pgUsd = env.H5_USE_BACKEND_COST === '1' ? await readPgCost(sessionId) : null;
|
||||
if (pgUsd != null) {
|
||||
return {
|
||||
...(sessionCost && typeof sessionCost === 'object' ? sessionCost : {}),
|
||||
accumulated_cost: pgUsd,
|
||||
};
|
||||
}
|
||||
return sessionCost;
|
||||
}
|
||||
|
||||
export async function resolveBillingTokenState(
|
||||
tokenStateRaw,
|
||||
{ sessionId = null, fetchSession = null } = {},
|
||||
{ sessionId = null, fetchSession = null, fetchSessionCostFromPg = null } = {},
|
||||
env = process.env,
|
||||
) {
|
||||
const state = normalizeTokenState(tokenStateRaw);
|
||||
|
||||
if (typeof fetchSession === 'function' && sessionId) {
|
||||
try {
|
||||
const session = await fetchSession(sessionId);
|
||||
const enriched = enrichTokenStateForBilling(state, { sessionCost: session }, env);
|
||||
if (enriched.accumulatedCost != null) return enriched;
|
||||
} catch {
|
||||
// Best-effort: fall through to inline cost / token estimate.
|
||||
}
|
||||
}
|
||||
|
||||
return enrichTokenStateForBilling(state, {}, env);
|
||||
const sessionCost = await resolveSessionCostPayload(
|
||||
sessionId,
|
||||
fetchSession,
|
||||
fetchSessionCostFromPg,
|
||||
env,
|
||||
);
|
||||
return enrichTokenStateForBilling(state, { sessionCost }, env);
|
||||
}
|
||||
|
||||
@@ -103,6 +103,23 @@ test('resolveBillingTokenState replaces inflated Finish cost with session cost',
|
||||
assert.equal(resolved.accumulatedCost, 0.006738208);
|
||||
});
|
||||
|
||||
test('resolveBillingTokenState uses PG cost when goosed session API omits accumulated_cost', async () => {
|
||||
const resolved = await resolveBillingTokenState(
|
||||
{
|
||||
accumulatedInputTokens: 280030,
|
||||
accumulatedOutputTokens: 5797,
|
||||
accumulatedCost: 0.082,
|
||||
},
|
||||
{
|
||||
sessionId: '20260804_19',
|
||||
fetchSession: async () => ({ id: '20260804_19', accumulated_cost: null }),
|
||||
fetchSessionCostFromPg: async () => 0.006738208,
|
||||
},
|
||||
{ H5_USE_BACKEND_COST: '1' },
|
||||
);
|
||||
assert.equal(resolved.accumulatedCost, 0.006738208);
|
||||
});
|
||||
|
||||
test('enriched cost drives 1.2x billing instead of flat fallback', () => {
|
||||
const previous = { lastInputTokens: 224853, lastOutputTokens: 6685, lastAccumulatedCost: null };
|
||||
const tokenState = enrichTokenStateForBilling(
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
let sharedPgClientPromise = null;
|
||||
|
||||
export function resolveGooseSessionPgUrl(env = process.env) {
|
||||
const explicit = String(env.GOOSE_SESSION_DB_URL ?? '').trim();
|
||||
if (explicit) return explicit;
|
||||
const host = env.GOOSE_SESSION_PG_HOST ?? '127.0.0.1';
|
||||
const port = env.GOOSE_SESSION_PG_PORT ?? '5432';
|
||||
const database = env.GOOSE_SESSION_PG_DATABASE ?? 'memind_sessions';
|
||||
const user = env.GOOSE_SESSION_PG_USER ?? 'john';
|
||||
const password = env.GOOSE_SESSION_PG_PASSWORD ?? '';
|
||||
if (password) {
|
||||
return `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${database}`;
|
||||
}
|
||||
return `postgresql://${user}@${host}:${port}/${database}`;
|
||||
}
|
||||
|
||||
export function isGooseSessionPgConfigured(env = process.env) {
|
||||
if (String(env.GOOSE_SESSION_DB_URL ?? '').trim()) return true;
|
||||
return String(env.GOOSE_SESSION_PG_DISABLE ?? '') !== '1';
|
||||
}
|
||||
|
||||
async function getSharedPgClient(env = process.env) {
|
||||
if (!isGooseSessionPgConfigured(env)) return null;
|
||||
if (!sharedPgClientPromise) {
|
||||
sharedPgClientPromise = import('pg')
|
||||
.then(async ({ default: pg }) => {
|
||||
const client = new pg.Client({ connectionString: resolveGooseSessionPgUrl(env) });
|
||||
await client.connect();
|
||||
return client;
|
||||
})
|
||||
.catch((err) => {
|
||||
sharedPgClientPromise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return sharedPgClientPromise;
|
||||
}
|
||||
|
||||
export async function fetchGooseSessionAccumulatedCostUsd(sessionId, env = process.env) {
|
||||
const normalizedSessionId = String(sessionId ?? '').trim();
|
||||
if (!normalizedSessionId || !isGooseSessionPgConfigured(env)) return null;
|
||||
try {
|
||||
const client = await getSharedPgClient(env);
|
||||
const result = await client.query(
|
||||
`SELECT accumulated_cost FROM sessions WHERE id = $1 LIMIT 1`,
|
||||
[normalizedSessionId],
|
||||
);
|
||||
const raw = result.rows[0]?.accumulated_cost;
|
||||
if (raw == null) return null;
|
||||
const value = Number(raw);
|
||||
return Number.isFinite(value) && value >= 0 ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function createGooseSessionCostReader(env = process.env) {
|
||||
return (sessionId) => fetchGooseSessionAccumulatedCostUsd(sessionId, env);
|
||||
}
|
||||
|
||||
export async function closeGooseSessionPgClient() {
|
||||
if (!sharedPgClientPromise) return;
|
||||
try {
|
||||
const client = await sharedPgClientPromise;
|
||||
await client.end();
|
||||
} catch {
|
||||
// ignore shutdown errors in tests/scripts
|
||||
} finally {
|
||||
sharedPgClientPromise = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
isGooseSessionPgConfigured,
|
||||
resolveGooseSessionPgUrl,
|
||||
} from './goose-session-cost.mjs';
|
||||
|
||||
test('resolveGooseSessionPgUrl prefers GOOSE_SESSION_DB_URL', () => {
|
||||
assert.equal(
|
||||
resolveGooseSessionPgUrl({ GOOSE_SESSION_DB_URL: 'postgresql://u:p@host:5432/db' }),
|
||||
'postgresql://u:p@host:5432/db',
|
||||
);
|
||||
});
|
||||
|
||||
test('resolveGooseSessionPgUrl builds local default DSN', () => {
|
||||
assert.equal(
|
||||
resolveGooseSessionPgUrl({
|
||||
GOOSE_SESSION_PG_HOST: '127.0.0.1',
|
||||
GOOSE_SESSION_PG_PORT: '5432',
|
||||
GOOSE_SESSION_PG_DATABASE: 'memind_sessions',
|
||||
GOOSE_SESSION_PG_USER: 'john',
|
||||
}),
|
||||
'postgresql://john@127.0.0.1:5432/memind_sessions',
|
||||
);
|
||||
});
|
||||
|
||||
test('isGooseSessionPgConfigured can be disabled explicitly', () => {
|
||||
assert.equal(isGooseSessionPgConfigured({ GOOSE_SESSION_PG_DISABLE: '1' }), false);
|
||||
assert.equal(isGooseSessionPgConfigured({ GOOSE_SESSION_DB_URL: 'postgresql://x' }), true);
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import mysql from 'mysql2/promise';
|
||||
import pg from 'pg';
|
||||
import { resolveGooseSessionPgUrl } from '../goose-session-cost.mjs';
|
||||
|
||||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
@@ -33,9 +34,14 @@ loadEnvFile(path.join(root, '.env'));
|
||||
|
||||
const apply = process.argv.includes('--apply');
|
||||
const sinceArg = process.argv.find((a) => a.startsWith('--since='));
|
||||
const sinceDate = sinceArg ? sinceArg.slice('--since='.length) : '2026-08-04';
|
||||
const startMs = new Date(`${sinceDate}T00:00:00+08:00`).getTime();
|
||||
const DEDupe_NOTE_PREFIX = `补偿:Token估价超扣(${sinceDate}起)`;
|
||||
const sinceRaw = sinceArg ? sinceArg.slice('--since='.length) : '2026-08-04';
|
||||
const startMs = sinceRaw.includes('T')
|
||||
? new Date(sinceRaw).getTime()
|
||||
: new Date(`${sinceRaw}T00:00:00+08:00`).getTime();
|
||||
const sinceLabel = sinceRaw.includes('T')
|
||||
? sinceRaw.replace('T', ' ').replace('+08:00', ' CST')
|
||||
: `${sinceRaw} 00:00 CST`;
|
||||
const DEDupe_NOTE_PREFIX = `补偿:Token估价超扣(${sinceLabel}起)`;
|
||||
|
||||
function loadBillingConfig() {
|
||||
const marginMultiplier = Number(process.env.H5_MARGIN_MULTIPLIER ?? 1);
|
||||
@@ -56,12 +62,7 @@ function correctTotalCents(gooseCostUsd, config) {
|
||||
}
|
||||
|
||||
function resolvePgUrl() {
|
||||
if (process.env.GOOSE_SESSION_DB_URL) return process.env.GOOSE_SESSION_DB_URL;
|
||||
const host = process.env.GOOSE_SESSION_PG_HOST ?? '127.0.0.1';
|
||||
const port = process.env.GOOSE_SESSION_PG_PORT ?? '5432';
|
||||
const db = process.env.GOOSE_SESSION_PG_DATABASE ?? 'memind_sessions';
|
||||
const user = process.env.GOOSE_SESSION_PG_USER ?? 'john';
|
||||
return `postgresql://${user}@${host}:${port}/${db}`;
|
||||
return resolveGooseSessionPgUrl(process.env);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
@@ -102,7 +103,7 @@ async function main() {
|
||||
|
||||
const sessionIds = [...bySession.keys()];
|
||||
if (sessionIds.length === 0) {
|
||||
console.log('No usage records since', sinceDate);
|
||||
console.log('No usage records since', sinceLabel);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -150,7 +151,7 @@ async function main() {
|
||||
const totalRefund = users.reduce((sum, user) => sum + user.refundCents, 0);
|
||||
|
||||
console.log('Billing config:', config);
|
||||
console.log('Since:', sinceDate, `(${startMs})`);
|
||||
console.log('Since:', sinceLabel, `(${startMs})`);
|
||||
console.log('Affected users:', users.length);
|
||||
console.log('Total refund:', `¥${(totalRefund / 100).toFixed(2)}`, `(${totalRefund} cents)`);
|
||||
console.log('');
|
||||
|
||||
Reference in New Issue
Block a user