Files
memind/billing-token-state.mjs
T
john 8c1ae7550d 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>
2026-08-05 15:48:39 +08:00

101 lines
3.4 KiB
JavaScript

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';
const enabled = useBackendCost && env.H5_COST_ESTIMATE_FROM_TOKENS !== '0';
const inputUsdPer1M = Number(env.H5_COST_ESTIMATE_INPUT_USD_PER_1M ?? 0.27);
const outputUsdPer1M = Number(env.H5_COST_ESTIMATE_OUTPUT_USD_PER_1M ?? 1.1);
return {
enabled,
inputUsdPer1M: Number.isFinite(inputUsdPer1M) && inputUsdPer1M >= 0 ? inputUsdPer1M : 0.27,
outputUsdPer1M: Number.isFinite(outputUsdPer1M) && outputUsdPer1M >= 0 ? outputUsdPer1M : 1.1,
};
}
export function pickSessionAccumulatedCost(session) {
if (!session || typeof session !== 'object') return null;
const raw = session.accumulatedCost ?? session.accumulated_cost ?? null;
if (raw == null) return null;
const value = Number(raw);
return Number.isFinite(value) && value >= 0 ? value : null;
}
export function estimateAccumulatedCostUsd(tokenStateRaw, estimateConfig = loadCostEstimateConfig()) {
if (!estimateConfig?.enabled) return null;
const state = normalizeTokenState(tokenStateRaw);
if (state.accumulatedInputTokens <= 0 && state.accumulatedOutputTokens <= 0) return null;
const inputUsd =
(state.accumulatedInputTokens * estimateConfig.inputUsdPer1M) / 1_000_000;
const outputUsd =
(state.accumulatedOutputTokens * estimateConfig.outputUsdPer1M) / 1_000_000;
const total = inputUsd + outputUsd;
return total > 0 ? total : null;
}
export function enrichTokenStateForBilling(
tokenStateRaw,
{ sessionCost = null } = {},
env = process.env,
) {
const state = normalizeTokenState(tokenStateRaw);
const sessionUsd = pickSessionAccumulatedCost(sessionCost);
if (sessionUsd != null) {
return { ...state, accumulatedCost: sessionUsd };
}
if (state.accumulatedCost != null && Number(state.accumulatedCost) >= 0) {
return state;
}
const estimatedUsd = estimateAccumulatedCostUsd(state, loadCostEstimateConfig(env));
if (estimatedUsd != null) {
return { ...state, accumulatedCost: estimatedUsd };
}
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, fetchSessionCostFromPg = null } = {},
env = process.env,
) {
const state = normalizeTokenState(tokenStateRaw);
const sessionCost = await resolveSessionCostPayload(
sessionId,
fetchSession,
fetchSessionCostFromPg,
env,
);
return enrichTokenStateForBilling(state, { sessionCost }, env);
}