| 1177 | } |
| 1178 | |
| 1179 | private async recordExecutionUsage( |
| 1180 | workflowId: string | null, |
| 1181 | costSummary: { |
| 1182 | totalCost: number |
| 1183 | totalInputCost: number |
| 1184 | totalOutputCost: number |
| 1185 | totalTokens: number |
| 1186 | totalPromptTokens: number |
| 1187 | totalCompletionTokens: number |
| 1188 | baseExecutionCharge: number |
| 1189 | models?: Record< |
| 1190 | string, |
| 1191 | { |
| 1192 | input: number |
| 1193 | output: number |
| 1194 | total: number |
| 1195 | toolCost?: number |
| 1196 | tokens: { input: number; output: number; total: number } |
| 1197 | } |
| 1198 | > |
| 1199 | charges?: Record<string, { total: number }> |
| 1200 | }, |
| 1201 | trigger: ExecutionTrigger['type'], |
| 1202 | executionId?: string, |
| 1203 | billingUserId?: string | null, |
| 1204 | // Pre-resolved billing context. The completion path already fetches the |
| 1205 | // subscription for usage-threshold emails; passing the derived context here |
| 1206 | // lets recordUsage skip a redundant subscription lookup per completion. |
| 1207 | billingContext?: BillingContext |
| 1208 | ): Promise<number> { |
| 1209 | const statsLog = logger.withMetadata({ workflowId: workflowId ?? undefined, executionId }) |
| 1210 | |
| 1211 | // The usage ledger (recordUsage below) is written regardless of |
| 1212 | // BILLING_ENABLED so cost is available everywhere (incl. self-hosted). |
| 1213 | // Only enforcement (overage/Stripe) is gated on the flag. |
| 1214 | // Returns the amount actually recorded at THIS boundary (the increment), so |
| 1215 | // callers drive usage-threshold math off the delta rather than the |
| 1216 | // cumulative run total (which would double-count pre-pause cost on resume). |
| 1217 | |
| 1218 | if (!workflowId) { |
| 1219 | statsLog.debug('Workflow was deleted, skipping usage recording') |
| 1220 | return 0 |
| 1221 | } |
| 1222 | |
| 1223 | let recordedIncrement = 0 |
| 1224 | try { |
| 1225 | const [workflowRecord] = await db |
| 1226 | .select() |
| 1227 | .from(workflow) |
| 1228 | .where(eq(workflow.id, workflowId)) |
| 1229 | .limit(1) |
| 1230 | |
| 1231 | if (!workflowRecord) { |
| 1232 | statsLog.error('Workflow not found for usage recording') |
| 1233 | return 0 |
| 1234 | } |
| 1235 | |
| 1236 | const userId = billingUserId?.trim() || null |