@dexcost/sdk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/dist/adapters/_netbytes.d.ts +31 -0
- package/dist/adapters/_netbytes.d.ts.map +1 -0
- package/dist/adapters/_netbytes.js +154 -0
- package/dist/adapters/_netbytes.js.map +1 -0
- package/dist/adapters/aws-lambda.d.ts +41 -0
- package/dist/adapters/aws-lambda.d.ts.map +1 -0
- package/dist/adapters/aws-lambda.js +65 -0
- package/dist/adapters/aws-lambda.js.map +1 -0
- package/dist/adapters/browser.d.ts +52 -0
- package/dist/adapters/browser.d.ts.map +1 -0
- package/dist/adapters/browser.js +127 -0
- package/dist/adapters/browser.js.map +1 -0
- package/dist/adapters/compute-wrap.d.ts +33 -0
- package/dist/adapters/compute-wrap.d.ts.map +1 -0
- package/dist/adapters/compute-wrap.js +188 -0
- package/dist/adapters/compute-wrap.js.map +1 -0
- package/dist/adapters/data/aws_lambda_pricing.json +61 -0
- package/dist/adapters/gpu-wrap.d.ts +31 -0
- package/dist/adapters/gpu-wrap.d.ts.map +1 -0
- package/dist/adapters/gpu-wrap.js +147 -0
- package/dist/adapters/gpu-wrap.js.map +1 -0
- package/dist/adapters/http.d.ts +58 -0
- package/dist/adapters/http.d.ts.map +1 -0
- package/dist/adapters/http.js +769 -0
- package/dist/adapters/http.js.map +1 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +12 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/network-accountant.d.ts +63 -0
- package/dist/adapters/network-accountant.d.ts.map +1 -0
- package/dist/adapters/network-accountant.js +153 -0
- package/dist/adapters/network-accountant.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +225 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/scanner.d.ts +39 -0
- package/dist/cli/scanner.d.ts.map +1 -0
- package/dist/cli/scanner.js +480 -0
- package/dist/cli/scanner.js.map +1 -0
- package/dist/clients.d.ts +54 -0
- package/dist/clients.d.ts.map +1 -0
- package/dist/clients.js +240 -0
- package/dist/clients.js.map +1 -0
- package/dist/cloud-detect.d.ts +96 -0
- package/dist/cloud-detect.d.ts.map +1 -0
- package/dist/cloud-detect.js +545 -0
- package/dist/cloud-detect.js.map +1 -0
- package/dist/core/auto-task.d.ts +20 -0
- package/dist/core/auto-task.d.ts.map +1 -0
- package/dist/core/auto-task.js +34 -0
- package/dist/core/auto-task.js.map +1 -0
- package/dist/core/cgroup-reader.d.ts +45 -0
- package/dist/core/cgroup-reader.d.ts.map +1 -0
- package/dist/core/cgroup-reader.js +124 -0
- package/dist/core/cgroup-reader.js.map +1 -0
- package/dist/core/cgroup-walker.d.ts +60 -0
- package/dist/core/cgroup-walker.d.ts.map +1 -0
- package/dist/core/cgroup-walker.js +166 -0
- package/dist/core/cgroup-walker.js.map +1 -0
- package/dist/core/compute-accountant.d.ts +51 -0
- package/dist/core/compute-accountant.d.ts.map +1 -0
- package/dist/core/compute-accountant.js +179 -0
- package/dist/core/compute-accountant.js.map +1 -0
- package/dist/core/compute-runtime.d.ts +42 -0
- package/dist/core/compute-runtime.d.ts.map +1 -0
- package/dist/core/compute-runtime.js +80 -0
- package/dist/core/compute-runtime.js.map +1 -0
- package/dist/core/config.d.ts +44 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +66 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/context.d.ts +76 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +91 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/fargate-metadata.d.ts +27 -0
- package/dist/core/fargate-metadata.d.ts.map +1 -0
- package/dist/core/fargate-metadata.js +102 -0
- package/dist/core/fargate-metadata.js.map +1 -0
- package/dist/core/gpu-accountant.d.ts +104 -0
- package/dist/core/gpu-accountant.d.ts.map +1 -0
- package/dist/core/gpu-accountant.js +383 -0
- package/dist/core/gpu-accountant.js.map +1 -0
- package/dist/core/gpu-runtime.d.ts +58 -0
- package/dist/core/gpu-runtime.d.ts.map +1 -0
- package/dist/core/gpu-runtime.js +131 -0
- package/dist/core/gpu-runtime.js.map +1 -0
- package/dist/core/heuristics.d.ts +74 -0
- package/dist/core/heuristics.d.ts.map +1 -0
- package/dist/core/heuristics.js +182 -0
- package/dist/core/heuristics.js.map +1 -0
- package/dist/core/models.d.ts +149 -0
- package/dist/core/models.d.ts.map +1 -0
- package/dist/core/models.js +226 -0
- package/dist/core/models.js.map +1 -0
- package/dist/core/nvml-reader.d.ts +114 -0
- package/dist/core/nvml-reader.d.ts.map +1 -0
- package/dist/core/nvml-reader.js +323 -0
- package/dist/core/nvml-reader.js.map +1 -0
- package/dist/core/session.d.ts +48 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +123 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/tracker.d.ts +364 -0
- package/dist/core/tracker.d.ts.map +1 -0
- package/dist/core/tracker.js +1073 -0
- package/dist/core/tracker.js.map +1 -0
- package/dist/data/compute_prices.json +180 -0
- package/dist/data/egress_prices.json +418 -0
- package/dist/data/gpu_prices.json +412 -0
- package/dist/data/service_prices.json +2595 -0
- package/dist/dev-console.d.ts +12 -0
- package/dist/dev-console.d.ts.map +1 -0
- package/dist/dev-console.js +60 -0
- package/dist/dev-console.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/instruments/anthropic.d.ts +26 -0
- package/dist/instruments/anthropic.d.ts.map +1 -0
- package/dist/instruments/anthropic.js +242 -0
- package/dist/instruments/anthropic.js.map +1 -0
- package/dist/instruments/bedrock.d.ts +29 -0
- package/dist/instruments/bedrock.d.ts.map +1 -0
- package/dist/instruments/bedrock.js +215 -0
- package/dist/instruments/bedrock.js.map +1 -0
- package/dist/instruments/cohere.d.ts +29 -0
- package/dist/instruments/cohere.d.ts.map +1 -0
- package/dist/instruments/cohere.js +237 -0
- package/dist/instruments/cohere.js.map +1 -0
- package/dist/instruments/gemini.d.ts +30 -0
- package/dist/instruments/gemini.d.ts.map +1 -0
- package/dist/instruments/gemini.js +247 -0
- package/dist/instruments/gemini.js.map +1 -0
- package/dist/instruments/index.d.ts +35 -0
- package/dist/instruments/index.d.ts.map +1 -0
- package/dist/instruments/index.js +54 -0
- package/dist/instruments/index.js.map +1 -0
- package/dist/instruments/mcp.d.ts +24 -0
- package/dist/instruments/mcp.d.ts.map +1 -0
- package/dist/instruments/mcp.js +459 -0
- package/dist/instruments/mcp.js.map +1 -0
- package/dist/instruments/openai.d.ts +26 -0
- package/dist/instruments/openai.d.ts.map +1 -0
- package/dist/instruments/openai.js +221 -0
- package/dist/instruments/openai.js.map +1 -0
- package/dist/instruments/vercel-ai.d.ts +28 -0
- package/dist/instruments/vercel-ai.d.ts.map +1 -0
- package/dist/instruments/vercel-ai.js +192 -0
- package/dist/instruments/vercel-ai.js.map +1 -0
- package/dist/integrations/langchain.d.ts +65 -0
- package/dist/integrations/langchain.d.ts.map +1 -0
- package/dist/integrations/langchain.js +165 -0
- package/dist/integrations/langchain.js.map +1 -0
- package/dist/middleware/express.d.ts +55 -0
- package/dist/middleware/express.d.ts.map +1 -0
- package/dist/middleware/express.js +101 -0
- package/dist/middleware/express.js.map +1 -0
- package/dist/middleware/index.d.ts +6 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +5 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/pricing/compute-pricing.d.ts +57 -0
- package/dist/pricing/compute-pricing.d.ts.map +1 -0
- package/dist/pricing/compute-pricing.js +627 -0
- package/dist/pricing/compute-pricing.js.map +1 -0
- package/dist/pricing/cost_map.json +37665 -0
- package/dist/pricing/egress-pricing.d.ts +55 -0
- package/dist/pricing/egress-pricing.d.ts.map +1 -0
- package/dist/pricing/egress-pricing.js +226 -0
- package/dist/pricing/egress-pricing.js.map +1 -0
- package/dist/pricing/engine.d.ts +24 -0
- package/dist/pricing/engine.d.ts.map +1 -0
- package/dist/pricing/engine.js +148 -0
- package/dist/pricing/engine.js.map +1 -0
- package/dist/pricing/gpu-pricing.d.ts +63 -0
- package/dist/pricing/gpu-pricing.d.ts.map +1 -0
- package/dist/pricing/gpu-pricing.js +484 -0
- package/dist/pricing/gpu-pricing.js.map +1 -0
- package/dist/pricing/rates.d.ts +17 -0
- package/dist/pricing/rates.d.ts.map +1 -0
- package/dist/pricing/rates.js +102 -0
- package/dist/pricing/rates.js.map +1 -0
- package/dist/pricing/service-catalog.d.ts +87 -0
- package/dist/pricing/service-catalog.d.ts.map +1 -0
- package/dist/pricing/service-catalog.js +406 -0
- package/dist/pricing/service-catalog.js.map +1 -0
- package/dist/schema/dexcost-event.v1.json +111 -0
- package/dist/schema/dexcost-task.v1.json +160 -0
- package/dist/schema/validate.d.ts +15 -0
- package/dist/schema/validate.d.ts.map +1 -0
- package/dist/schema/validate.js +87 -0
- package/dist/schema/validate.js.map +1 -0
- package/dist/security/redaction.d.ts +55 -0
- package/dist/security/redaction.d.ts.map +1 -0
- package/dist/security/redaction.js +144 -0
- package/dist/security/redaction.js.map +1 -0
- package/dist/transport/buffer.d.ts +117 -0
- package/dist/transport/buffer.d.ts.map +1 -0
- package/dist/transport/buffer.js +759 -0
- package/dist/transport/buffer.js.map +1 -0
- package/dist/transport/pusher.d.ts +89 -0
- package/dist/transport/pusher.d.ts.map +1 -0
- package/dist/transport/pusher.js +323 -0
- package/dist/transport/pusher.js.map +1 -0
- package/package.json +93 -0
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CostTracker — the main entry point for recording AI agent costs.
|
|
3
|
+
*
|
|
4
|
+
* Wraps business logic in tracked tasks, records cost events, and
|
|
5
|
+
* manages background flushing to a remote endpoint.
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { Decimal } from "decimal.js";
|
|
9
|
+
/**
|
|
10
|
+
* Decimal-based addition to defeat floating-point drift in cost
|
|
11
|
+
* accumulation. Sprint 2 Theme E / §3.3.1 (B3).
|
|
12
|
+
*
|
|
13
|
+
* Native `a + b` on `number` accumulates ~2e-16 of error per add;
|
|
14
|
+
* over 10 000 events that adds up to a visible drift in the per-
|
|
15
|
+
* task total. decimal.js does exact decimal arithmetic; we convert
|
|
16
|
+
* back to `number` at the boundary so the customer-facing field
|
|
17
|
+
* type stays `number` (full `number → string` wire-format change
|
|
18
|
+
* is deferred to a major version per plan §0.7).
|
|
19
|
+
*/
|
|
20
|
+
function decAdd(a, b) {
|
|
21
|
+
return new Decimal(a).plus(b).toNumber();
|
|
22
|
+
}
|
|
23
|
+
import { createTask, createCostEvent } from "./models.js";
|
|
24
|
+
import { getCurrentTask, runWithTask } from "./context.js";
|
|
25
|
+
import { EventBuffer } from "../transport/buffer.js";
|
|
26
|
+
import { NetworkAccountant, registerAccountant, unregisterAccountant, } from "../adapters/network-accountant.js";
|
|
27
|
+
import { EgressPricingEngine } from "../pricing/egress-pricing.js";
|
|
28
|
+
import { ComputePricingEngine } from "../pricing/compute-pricing.js";
|
|
29
|
+
import { RuntimeKind } from "./compute-runtime.js";
|
|
30
|
+
import { GpuPricingEngine } from "../pricing/gpu-pricing.js";
|
|
31
|
+
import { GpuRuntimeKind } from "./gpu-runtime.js";
|
|
32
|
+
import { getCloudEnv } from "../cloud-detect.js";
|
|
33
|
+
import { EventPusher } from "../transport/pusher.js";
|
|
34
|
+
import { PricingEngine } from "../pricing/engine.js";
|
|
35
|
+
import { RateRegistry } from "../pricing/rates.js";
|
|
36
|
+
import { RetryHeuristicEngine } from "./heuristics.js";
|
|
37
|
+
import { resolveConfig } from "./config.js";
|
|
38
|
+
import { ALL_SUPPORTED_INSTRUMENTS, instrumentProvider, uninstrumentProvider, } from "../instruments/index.js";
|
|
39
|
+
export const DEFAULT_ENDPOINT = "https://api.dexcost.io";
|
|
40
|
+
/**
|
|
41
|
+
* Resolves the Control Layer endpoint from the DEXCOST_ENDPOINT env
|
|
42
|
+
* var. Sprint 1 Theme A / §2.1 (A2): only https:// URLs are accepted.
|
|
43
|
+
* An attacker who controls the env (misconfigured CI runner, hostile
|
|
44
|
+
* container) could otherwise silently exfiltrate cost telemetry to an
|
|
45
|
+
* HTTP collector — we refuse and fall back to the production default
|
|
46
|
+
* with a console.warn.
|
|
47
|
+
*
|
|
48
|
+
* Exported for testability; the CostTracker constructor is the only
|
|
49
|
+
* production caller.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveEndpoint() {
|
|
52
|
+
const env = process.env.DEXCOST_ENDPOINT;
|
|
53
|
+
if (env === undefined || env === "") {
|
|
54
|
+
return DEFAULT_ENDPOINT;
|
|
55
|
+
}
|
|
56
|
+
if (!env.startsWith("https://")) {
|
|
57
|
+
console.warn(`dexcost: DEXCOST_ENDPOINT=${JSON.stringify(env)} rejected — only ` +
|
|
58
|
+
`https:// URLs are accepted. Falling back to ${DEFAULT_ENDPOINT}.`);
|
|
59
|
+
return DEFAULT_ENDPOINT;
|
|
60
|
+
}
|
|
61
|
+
return env;
|
|
62
|
+
}
|
|
63
|
+
/** Event types accepted by `recordCost` (non-LLM cost events). */
|
|
64
|
+
const NON_LLM_EVENT_TYPES = new Set(["external_cost", "compute_cost"]);
|
|
65
|
+
import { isDevMode, enableDevMode, logEvent, logTaskComplete } from "../dev-console.js";
|
|
66
|
+
// Side-effect imports to register instruments
|
|
67
|
+
import "../instruments/openai.js";
|
|
68
|
+
import "../instruments/anthropic.js";
|
|
69
|
+
import "../instruments/vercel-ai.js";
|
|
70
|
+
import "../instruments/gemini.js";
|
|
71
|
+
import "../instruments/bedrock.js";
|
|
72
|
+
import "../instruments/cohere.js";
|
|
73
|
+
import "../instruments/mcp.js";
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Singleton / init() factory
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
let _instance = null;
|
|
78
|
+
/**
|
|
79
|
+
* Sprint 2 Theme E / §3.3.2 (B9) — exit-time flush handlers.
|
|
80
|
+
*
|
|
81
|
+
* Pre-fix events recorded just before `process.exit(0)` were lost:
|
|
82
|
+
* the buffered in-memory queue and the not-yet-flushed pusher batch
|
|
83
|
+
* both died with the process. These handlers run on process tear-
|
|
84
|
+
* down (graceful exit, SIGTERM, SIGINT) and synchronously close the
|
|
85
|
+
* tracker. closeAsync() flushes the pending push first.
|
|
86
|
+
*
|
|
87
|
+
* The handlers are stored so `close()` can unregister them — avoids
|
|
88
|
+
* cross-test listener-leak when init/close cycles repeatedly.
|
|
89
|
+
*/
|
|
90
|
+
let _exitHandlers = null;
|
|
91
|
+
function _registerExitHandlers() {
|
|
92
|
+
if (_exitHandlers !== null)
|
|
93
|
+
return;
|
|
94
|
+
const beforeExit = (_code) => {
|
|
95
|
+
// Synchronous best-effort flush on graceful exit. Node will wait
|
|
96
|
+
// for any returned promise from `beforeExit` (unlike `exit`), so
|
|
97
|
+
// closeAsync's in-flight push has a chance to land.
|
|
98
|
+
void globalCloseAsync();
|
|
99
|
+
};
|
|
100
|
+
const sigterm = () => {
|
|
101
|
+
// SIGTERM: containerized environments (k8s, docker stop) deliver
|
|
102
|
+
// this 30s before SIGKILL. Run closeAsync to flush, then let the
|
|
103
|
+
// default handler take over (re-emit so other listeners run).
|
|
104
|
+
void globalCloseAsync();
|
|
105
|
+
};
|
|
106
|
+
const sigint = () => {
|
|
107
|
+
// SIGINT (Ctrl+C in dev): same flush guarantee.
|
|
108
|
+
void globalCloseAsync();
|
|
109
|
+
};
|
|
110
|
+
process.on("beforeExit", beforeExit);
|
|
111
|
+
process.on("SIGTERM", sigterm);
|
|
112
|
+
process.on("SIGINT", sigint);
|
|
113
|
+
_exitHandlers = { beforeExit, sigterm, sigint };
|
|
114
|
+
}
|
|
115
|
+
function _unregisterExitHandlers() {
|
|
116
|
+
if (_exitHandlers === null)
|
|
117
|
+
return;
|
|
118
|
+
if (_exitHandlers.beforeExit)
|
|
119
|
+
process.off("beforeExit", _exitHandlers.beforeExit);
|
|
120
|
+
if (_exitHandlers.sigterm)
|
|
121
|
+
process.off("SIGTERM", _exitHandlers.sigterm);
|
|
122
|
+
if (_exitHandlers.sigint)
|
|
123
|
+
process.off("SIGINT", _exitHandlers.sigint);
|
|
124
|
+
_exitHandlers = null;
|
|
125
|
+
}
|
|
126
|
+
export function init(options = {}) {
|
|
127
|
+
if (_instance !== null) {
|
|
128
|
+
throw new Error("dexcost already initialized — call close() first to reset");
|
|
129
|
+
}
|
|
130
|
+
_instance = new CostTracker(options);
|
|
131
|
+
_registerExitHandlers();
|
|
132
|
+
return _instance;
|
|
133
|
+
}
|
|
134
|
+
export function getTracker() {
|
|
135
|
+
if (_instance === null) {
|
|
136
|
+
throw new Error("dexcost not initialized — call init() first");
|
|
137
|
+
}
|
|
138
|
+
return _instance;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Update the SDK's API key and resume sync after auth failure.
|
|
142
|
+
*
|
|
143
|
+
* Sprint 2 Theme D / §3.2.3 (B14). When the Control Layer returns
|
|
144
|
+
* 401/403 the pusher sets `_authFailed=true` and stops; without this
|
|
145
|
+
* function the only recovery is restarting the customer's process.
|
|
146
|
+
*
|
|
147
|
+
* Returns `true` on success, `false` if `init()` has not been called
|
|
148
|
+
* (logs a console warning).
|
|
149
|
+
*/
|
|
150
|
+
export function setApiKey(newKey) {
|
|
151
|
+
if (_instance === null) {
|
|
152
|
+
console.warn("dexcost: setApiKey called before init(); ignoring. " +
|
|
153
|
+
"Call dexcost.init({apiKey:...}) first.");
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
_instance.setApiKey(newKey);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
export async function globalTrack(opts, fn) {
|
|
160
|
+
return getTracker().track(opts, fn);
|
|
161
|
+
}
|
|
162
|
+
export async function globalFlush() {
|
|
163
|
+
return getTracker().flush();
|
|
164
|
+
}
|
|
165
|
+
export function globalClose() {
|
|
166
|
+
if (_instance !== null) {
|
|
167
|
+
_instance.close();
|
|
168
|
+
_instance = null;
|
|
169
|
+
}
|
|
170
|
+
_unregisterExitHandlers();
|
|
171
|
+
}
|
|
172
|
+
export async function globalCloseAsync() {
|
|
173
|
+
if (_instance !== null) {
|
|
174
|
+
await _instance.closeAsync();
|
|
175
|
+
_instance = null;
|
|
176
|
+
}
|
|
177
|
+
_unregisterExitHandlers();
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A task that is currently being tracked.
|
|
181
|
+
*
|
|
182
|
+
* Provides methods to record cost events (LLM calls, external costs,
|
|
183
|
+
* retries) against the task.
|
|
184
|
+
*/
|
|
185
|
+
export class TrackedTask {
|
|
186
|
+
_task;
|
|
187
|
+
_buffer;
|
|
188
|
+
_tracker;
|
|
189
|
+
_events = [];
|
|
190
|
+
_ended = false;
|
|
191
|
+
constructor(task, buffer, tracker) {
|
|
192
|
+
this._task = task;
|
|
193
|
+
this._buffer = buffer;
|
|
194
|
+
this._tracker = tracker;
|
|
195
|
+
// Register a NetworkAccountant for this task so the patched
|
|
196
|
+
// globalThis.fetch (which sees only the task_id via AsyncLocalStorage)
|
|
197
|
+
// can record byte usage via core.getAccountant(taskId).
|
|
198
|
+
// Unregistered in end().
|
|
199
|
+
registerAccountant(task.taskId, new NetworkAccountant());
|
|
200
|
+
}
|
|
201
|
+
/** The underlying Task data. */
|
|
202
|
+
get task() {
|
|
203
|
+
return this._task;
|
|
204
|
+
}
|
|
205
|
+
/** All events recorded against this task. */
|
|
206
|
+
get events() {
|
|
207
|
+
return this._events;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Record an LLM call event.
|
|
211
|
+
*
|
|
212
|
+
* When `cost` is omitted, the cost is auto-computed via the pricing
|
|
213
|
+
* engine (mirrors Python `tracker.record_llm_call`). Accepts an
|
|
214
|
+
* options object for `error_type`, `details`, `pricingSource`, and
|
|
215
|
+
* `costConfidence`. `error_type` is stored in `details.error_type`.
|
|
216
|
+
*/
|
|
217
|
+
recordLlmCall(provider, model, inputTokens, outputTokens, cost, cachedTokens, latencyMs, options = {}) {
|
|
218
|
+
let costUsd;
|
|
219
|
+
let costConfidence;
|
|
220
|
+
let pricingSource;
|
|
221
|
+
let pricingVersion = options.pricingVersion;
|
|
222
|
+
if (cost === undefined) {
|
|
223
|
+
// Auto-compute via the pricing engine (mirrors Python US-010).
|
|
224
|
+
const result = this._tracker.pricing.getCost(model, inputTokens, outputTokens, cachedTokens ?? 0);
|
|
225
|
+
costUsd = result.costUsd;
|
|
226
|
+
costConfidence = options.costConfidence ?? result.costConfidence;
|
|
227
|
+
pricingSource = options.pricingSource ?? result.pricingSource;
|
|
228
|
+
pricingVersion = pricingVersion ?? result.pricingVersion;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
costUsd = cost;
|
|
232
|
+
costConfidence = options.costConfidence ?? "exact";
|
|
233
|
+
pricingSource = options.pricingSource ?? "manual";
|
|
234
|
+
}
|
|
235
|
+
const details = { ...(options.details ?? {}) };
|
|
236
|
+
if (options.errorType !== undefined) {
|
|
237
|
+
details.error_type = options.errorType;
|
|
238
|
+
}
|
|
239
|
+
const event = createCostEvent({
|
|
240
|
+
eventId: randomUUID(),
|
|
241
|
+
taskId: this._task.taskId,
|
|
242
|
+
eventType: "llm_call",
|
|
243
|
+
costUsd,
|
|
244
|
+
costConfidence,
|
|
245
|
+
pricingSource,
|
|
246
|
+
pricingVersion,
|
|
247
|
+
provider,
|
|
248
|
+
model,
|
|
249
|
+
inputTokens,
|
|
250
|
+
outputTokens,
|
|
251
|
+
cachedTokens,
|
|
252
|
+
latencyMs,
|
|
253
|
+
isRetry: false,
|
|
254
|
+
details,
|
|
255
|
+
});
|
|
256
|
+
// Heuristic retry detection — must run BEFORE the event is persisted so
|
|
257
|
+
// the SQLite row reflects the detected retry. Mirrors the Python SDK,
|
|
258
|
+
// which runs the heuristic engine before `insert_event` (sync.py /
|
|
259
|
+
// tracker.py). Running it after `addEvent` would persist is_retry=0 and
|
|
260
|
+
// any later update would be a separate, easy-to-drop write.
|
|
261
|
+
const engine = this._tracker.heuristicEngine;
|
|
262
|
+
if (engine && !event.isRetry) {
|
|
263
|
+
const match = engine.check(event);
|
|
264
|
+
if (match.isRetry) {
|
|
265
|
+
event.isRetry = true;
|
|
266
|
+
event.retryReason = match.reason || "heuristic";
|
|
267
|
+
event.retryOf = match.matchedEventId;
|
|
268
|
+
event.details = { ...event.details, retry_confidence: match.confidence };
|
|
269
|
+
this._task.retryCount += 1;
|
|
270
|
+
this._task.retryCostUsd = decAdd(this._task.retryCostUsd, costUsd);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Persist only after the retry fields have been finalised on `event`.
|
|
274
|
+
this._events.push(event);
|
|
275
|
+
this._buffer.addEvent(event);
|
|
276
|
+
logEvent(event, this._task.taskType);
|
|
277
|
+
// Feed the persisted event into the engine's sliding window.
|
|
278
|
+
if (engine) {
|
|
279
|
+
engine.record(event);
|
|
280
|
+
}
|
|
281
|
+
// Aggregate into task
|
|
282
|
+
this._task.llmCostUsd = decAdd(this._task.llmCostUsd, costUsd);
|
|
283
|
+
this._task.totalCostUsd = decAdd(this._task.totalCostUsd, costUsd);
|
|
284
|
+
this._task.totalInputTokens += inputTokens;
|
|
285
|
+
this._task.totalOutputTokens += outputTokens;
|
|
286
|
+
if (cachedTokens !== undefined) {
|
|
287
|
+
this._task.totalCachedTokens += cachedTokens;
|
|
288
|
+
}
|
|
289
|
+
this._buffer.upsertTask(this._task);
|
|
290
|
+
return event;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Record a non-LLM cost event (external API call, compute, etc.).
|
|
294
|
+
*
|
|
295
|
+
* `eventType` must be `"external_cost"` or `"compute_cost"`; any other
|
|
296
|
+
* value throws an Error (mirrors Python `tracker.record_cost`).
|
|
297
|
+
*/
|
|
298
|
+
recordCost(service, costUsd, details, eventType = "external_cost", costConfidence = "exact", pricingSource = "manual", pricingVersion) {
|
|
299
|
+
if (!NON_LLM_EVENT_TYPES.has(eventType)) {
|
|
300
|
+
throw new Error(`event_type must be one of ${[...NON_LLM_EVENT_TYPES].sort().join(", ")}, ` +
|
|
301
|
+
`got "${eventType}"`);
|
|
302
|
+
}
|
|
303
|
+
const event = createCostEvent({
|
|
304
|
+
eventId: randomUUID(),
|
|
305
|
+
taskId: this._task.taskId,
|
|
306
|
+
eventType,
|
|
307
|
+
costUsd,
|
|
308
|
+
costConfidence,
|
|
309
|
+
pricingSource,
|
|
310
|
+
pricingVersion,
|
|
311
|
+
serviceName: service,
|
|
312
|
+
isRetry: false,
|
|
313
|
+
details: details ?? {},
|
|
314
|
+
});
|
|
315
|
+
this._events.push(event);
|
|
316
|
+
this._buffer.addEvent(event);
|
|
317
|
+
logEvent(event, this._task.taskType);
|
|
318
|
+
// Aggregate into task
|
|
319
|
+
if (eventType === "external_cost") {
|
|
320
|
+
this._task.externalCostUsd = decAdd(this._task.externalCostUsd, costUsd);
|
|
321
|
+
}
|
|
322
|
+
else if (eventType === "compute_cost") {
|
|
323
|
+
this._task.computeCostUsd = decAdd(this._task.computeCostUsd, costUsd);
|
|
324
|
+
}
|
|
325
|
+
this._task.totalCostUsd = decAdd(this._task.totalCostUsd, costUsd);
|
|
326
|
+
this._buffer.upsertTask(this._task);
|
|
327
|
+
return event;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Record a retry event.
|
|
331
|
+
*/
|
|
332
|
+
markRetry(reason, cost, retryOf) {
|
|
333
|
+
const costUsd = cost ?? 0;
|
|
334
|
+
const event = createCostEvent({
|
|
335
|
+
eventId: randomUUID(),
|
|
336
|
+
taskId: this._task.taskId,
|
|
337
|
+
eventType: "retry_marker",
|
|
338
|
+
costUsd,
|
|
339
|
+
costConfidence: costUsd > 0 ? "exact" : "unknown",
|
|
340
|
+
isRetry: true,
|
|
341
|
+
retryReason: reason,
|
|
342
|
+
retryOf,
|
|
343
|
+
});
|
|
344
|
+
this._events.push(event);
|
|
345
|
+
this._buffer.addEvent(event);
|
|
346
|
+
logEvent(event, this._task.taskType);
|
|
347
|
+
// Aggregate into task
|
|
348
|
+
this._task.retryCount += 1;
|
|
349
|
+
this._task.retryCostUsd = decAdd(this._task.retryCostUsd, costUsd);
|
|
350
|
+
this._task.totalCostUsd = decAdd(this._task.totalCostUsd, costUsd);
|
|
351
|
+
this._buffer.upsertTask(this._task);
|
|
352
|
+
return event;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Link an external trace (e.g., Langfuse, LangSmith, Datadog) to this task.
|
|
356
|
+
*
|
|
357
|
+
* Stored under `metadata._trace_links` with `{ provider, trace_id }`
|
|
358
|
+
* entries — the same shape the Python SDK uses, so cross-SDK buffers
|
|
359
|
+
* interoperate.
|
|
360
|
+
*/
|
|
361
|
+
linkTrace(provider, traceId) {
|
|
362
|
+
if (!this._task.metadata["_trace_links"]) {
|
|
363
|
+
this._task.metadata["_trace_links"] = [];
|
|
364
|
+
}
|
|
365
|
+
this._task.metadata["_trace_links"].push({
|
|
366
|
+
provider,
|
|
367
|
+
trace_id: traceId,
|
|
368
|
+
});
|
|
369
|
+
this._buffer.upsertTask(this._task);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Return all linked traces for this task.
|
|
373
|
+
*
|
|
374
|
+
* Each entry is a `{ provider, trace_id }` object (mirrors Python
|
|
375
|
+
* `TrackedTask.get_trace_links`).
|
|
376
|
+
*/
|
|
377
|
+
getTraceLinks() {
|
|
378
|
+
const links = this._task.metadata["_trace_links"];
|
|
379
|
+
if (Array.isArray(links)) {
|
|
380
|
+
return links;
|
|
381
|
+
}
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* End the task, setting its status and ended_at timestamp.
|
|
386
|
+
*/
|
|
387
|
+
end(status = "success") {
|
|
388
|
+
if (this._ended) {
|
|
389
|
+
throw new Error(`Task ${this._task.taskId} has already been ended.`);
|
|
390
|
+
}
|
|
391
|
+
this._ended = true;
|
|
392
|
+
this._task.status = status;
|
|
393
|
+
this._task.endedAt = new Date();
|
|
394
|
+
if (status === "failed") {
|
|
395
|
+
this._task.failureCount += 1;
|
|
396
|
+
}
|
|
397
|
+
// ── Network finalize — v1 byte aggregates + v2 egress pricing ────
|
|
398
|
+
// Mirrors python tracker.py:_aggregate_costs + rust TrackedTask::
|
|
399
|
+
// finalize_network + go finalizeNetwork. Tier-5 fail-silent: any
|
|
400
|
+
// throw in the egress block is logged and swallowed so a pricing
|
|
401
|
+
// bug never breaks task finalization (the task still ships with
|
|
402
|
+
// v1 + LLM/external/compute costs intact).
|
|
403
|
+
try {
|
|
404
|
+
this._finalizeNetwork();
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
// eslint-disable-next-line no-console
|
|
408
|
+
console.warn(`[dexcost] egress cost computation failed for task ${this._task.taskId}:`, err);
|
|
409
|
+
this._task.networkCostUsd = 0;
|
|
410
|
+
}
|
|
411
|
+
// ── Compute capture (v1 + v2 cost) ───────────────────────────────────
|
|
412
|
+
// Long-running runtimes emit their compute_cost event at task finalize
|
|
413
|
+
// from the cgroup diff; serverless runtimes have already emitted from
|
|
414
|
+
// the handler wrap with cost_pending=true. Either way, the v2 pricing
|
|
415
|
+
// engine back-fills cost_usd here via the deferred-cost pattern.
|
|
416
|
+
// Wrapped in Tier-5 fail-silent so a pricing throw never breaks
|
|
417
|
+
// finalize (mirrors python tracker.py:_aggregate_costs +
|
|
418
|
+
// _finalize_compute).
|
|
419
|
+
try {
|
|
420
|
+
this._finalizeCompute();
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
// eslint-disable-next-line no-console
|
|
424
|
+
console.warn(`[dexcost] compute cost computation failed for task ${this._task.taskId}:`, err);
|
|
425
|
+
}
|
|
426
|
+
// ── GPU capture (Phase 2 v1 + v2) ─────────────────────────────────────
|
|
427
|
+
// Long-running GPU runtimes (AWS_EC2_GPU / GCP_GCE_BUNDLED / etc.) emit
|
|
428
|
+
// 1 gpu_cost + N gpu_utilization_signal at task finalize from the cgroup
|
|
429
|
+
// walk + NVML snapshot diff. Serverless runtimes (Modal / RunPod /
|
|
430
|
+
// Replicate) have already emitted via the handler wrap. Either way the
|
|
431
|
+
// GpuPricingEngine back-fills gpu_cost.costUsd here via the deferred-
|
|
432
|
+
// cost pattern. gpu_utilization_signal events are NEVER priced
|
|
433
|
+
// (Decision #3 observability carve-out). Tier-5 fail-silent.
|
|
434
|
+
try {
|
|
435
|
+
this._finalizeGpu();
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
// eslint-disable-next-line no-console
|
|
439
|
+
console.warn(`[dexcost] gpu cost computation failed for task ${this._task.taskId}:`, err);
|
|
440
|
+
}
|
|
441
|
+
this._buffer.upsertTask(this._task);
|
|
442
|
+
logTaskComplete(this._task);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Compute auto-emission + back-fill at task finalize.
|
|
446
|
+
*
|
|
447
|
+
* Mirrors python tracker.py:_finalize_compute.
|
|
448
|
+
*
|
|
449
|
+
* Step 1: long-running runtime → call snapshotEndAndBuild and insert a
|
|
450
|
+
* compute_cost event with details.cost_pending=true.
|
|
451
|
+
* Step 2: walk all compute_cost events with cost_pending=true, resolve
|
|
452
|
+
* their cost via the pricing engine, then updateEvent to strip
|
|
453
|
+
* the marker + stamp pricing source/confidence/version.
|
|
454
|
+
* Step 3: apply DELTA-based total adjustment — never recompute total_
|
|
455
|
+
* cost_usd from scratch, which would blow away retry_marker
|
|
456
|
+
* and other costs accumulated by the main loop.
|
|
457
|
+
*/
|
|
458
|
+
_finalizeCompute() {
|
|
459
|
+
const task = this._task;
|
|
460
|
+
const accountant = task._compute;
|
|
461
|
+
const cloudEnv = getCloudEnv();
|
|
462
|
+
const overrides = this._tracker.computeBillingOverrides;
|
|
463
|
+
let durationMs = 0;
|
|
464
|
+
let windowS = new Decimal(0);
|
|
465
|
+
if (task.endedAt && task.startedAt) {
|
|
466
|
+
const ms = task.endedAt.getTime() - task.startedAt.getTime();
|
|
467
|
+
durationMs = Math.trunc(ms);
|
|
468
|
+
windowS = new Decimal(ms).dividedBy(1000);
|
|
469
|
+
}
|
|
470
|
+
// 1. Long-running runtimes: build + persist the cgroup-diff event.
|
|
471
|
+
const longRunning = new Set([
|
|
472
|
+
RuntimeKind.Fargate,
|
|
473
|
+
RuntimeKind.Ec2,
|
|
474
|
+
RuntimeKind.Gce,
|
|
475
|
+
RuntimeKind.AzureVm,
|
|
476
|
+
RuntimeKind.K8sPod,
|
|
477
|
+
]);
|
|
478
|
+
const newEventIds = new Set();
|
|
479
|
+
if (accountant && longRunning.has(accountant.runtime)) {
|
|
480
|
+
const details = accountant.snapshotEndAndBuild(durationMs);
|
|
481
|
+
if (details !== null) {
|
|
482
|
+
const ev = createCostEvent({
|
|
483
|
+
eventId: randomUUID(),
|
|
484
|
+
taskId: task.taskId,
|
|
485
|
+
eventType: "compute_cost",
|
|
486
|
+
costUsd: 0,
|
|
487
|
+
costConfidence: "unknown",
|
|
488
|
+
isRetry: false,
|
|
489
|
+
details,
|
|
490
|
+
});
|
|
491
|
+
this._buffer.addEvent(ev);
|
|
492
|
+
this._events.push(ev);
|
|
493
|
+
newEventIds.add(ev.eventId);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// 2. Back-fill cost on every compute_cost event with cost_pending=true.
|
|
497
|
+
// Track per-event delta so we adjust totals without blowing away the
|
|
498
|
+
// running totals already accumulated by the main loop.
|
|
499
|
+
const engine = this._tracker.computePricing;
|
|
500
|
+
const events = this._buffer.queryEvents(task.taskId);
|
|
501
|
+
let costDelta = new Decimal(0);
|
|
502
|
+
for (const ev of events) {
|
|
503
|
+
if (ev.eventType !== "compute_cost")
|
|
504
|
+
continue;
|
|
505
|
+
const details = ev.details || {};
|
|
506
|
+
if (details.cost_pending !== true)
|
|
507
|
+
continue;
|
|
508
|
+
const oldCost = new Decimal(ev.costUsd);
|
|
509
|
+
const priced = engine.resolveComputeCost(details, cloudEnv, overrides, windowS);
|
|
510
|
+
ev.costUsd = priced.costUsd.toNumber();
|
|
511
|
+
ev.pricingSource = priced.pricingSource;
|
|
512
|
+
ev.costConfidence = priced.costConfidence;
|
|
513
|
+
ev.pricingVersion = `compute:${engine.catalogVersion}`;
|
|
514
|
+
const newDetails = {};
|
|
515
|
+
for (const [k, v] of Object.entries(details)) {
|
|
516
|
+
if (k !== "cost_pending")
|
|
517
|
+
newDetails[k] = v;
|
|
518
|
+
}
|
|
519
|
+
ev.details = newDetails;
|
|
520
|
+
this._buffer.updateEvent(ev);
|
|
521
|
+
// Delta = new - old. For newly-inserted long-running events the
|
|
522
|
+
// main loop never saw them at all, so we add the original $0 too
|
|
523
|
+
// (always 0 here, but explicit per python parity).
|
|
524
|
+
const delta = priced.costUsd.minus(oldCost);
|
|
525
|
+
costDelta = costDelta.plus(delta);
|
|
526
|
+
if (newEventIds.has(ev.eventId)) {
|
|
527
|
+
costDelta = costDelta.plus(oldCost);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const deltaNum = costDelta.toNumber();
|
|
531
|
+
task.computeCostUsd = decAdd(task.computeCostUsd, deltaNum);
|
|
532
|
+
task.totalCostUsd = decAdd(task.totalCostUsd, deltaNum);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* GPU auto-emission + back-fill at task finalize.
|
|
536
|
+
*
|
|
537
|
+
* Mirrors python tracker.py:_finalize_gpu. Three steps:
|
|
538
|
+
*
|
|
539
|
+
* 1. Long-running GPU runtimes (AwsEc2Gpu / GcpGceBundled /
|
|
540
|
+
* GcpGceN1Attached / AzureVmGpu / AzureVmVgpu / LambdaLabs /
|
|
541
|
+
* CoreWeave) call accountant.snapshotEndAndBuild(durationMs) and
|
|
542
|
+
* persist a gpu_cost event (cost_pending=true) plus N
|
|
543
|
+
* gpu_utilization_signal events. Serverless GPU runtimes (Modal /
|
|
544
|
+
* RunPod / Replicate) have already emitted via the handler wrap;
|
|
545
|
+
* this step is a no-op for them.
|
|
546
|
+
* 2. Back-fills cost_usd on every gpu_cost event with cost_pending=true:
|
|
547
|
+
* resolves rate via GpuPricingEngine.resolveGpuCost, sets cost_usd,
|
|
548
|
+
* pricing_source, cost_confidence, pricing_version ("gpu:<version>"
|
|
549
|
+
* — distinct from compute / egress prefixes), and strips the
|
|
550
|
+
* internal cost_pending / _cgroup_scope_fallback /
|
|
551
|
+
* _nvml_product_name_lower hints from details before re-persisting.
|
|
552
|
+
* 3. gpu_utilization_signal events are NEVER touched by the back-fill
|
|
553
|
+
* walker — they stay at cost_usd=0 (Decision #3 observability
|
|
554
|
+
* carve-out). Load-bearing convention §1 carve-out — see test
|
|
555
|
+
* gpu-auto-emission.test.ts.
|
|
556
|
+
*
|
|
557
|
+
* Delta-based total adjustment preserves any retry_marker costs already
|
|
558
|
+
* accumulated by the main aggregation loop.
|
|
559
|
+
*/
|
|
560
|
+
_finalizeGpu() {
|
|
561
|
+
const task = this._task;
|
|
562
|
+
const accountant = task._gpu;
|
|
563
|
+
const cloudEnv = getCloudEnv();
|
|
564
|
+
let durationMs = 0;
|
|
565
|
+
let windowS = new Decimal(0);
|
|
566
|
+
if (task.endedAt && task.startedAt) {
|
|
567
|
+
const ms = task.endedAt.getTime() - task.startedAt.getTime();
|
|
568
|
+
durationMs = Math.trunc(ms);
|
|
569
|
+
windowS = new Decimal(ms).dividedBy(1000);
|
|
570
|
+
}
|
|
571
|
+
// 1. Long-running GPU runtimes: snapshot + persist dual events.
|
|
572
|
+
const longRunningGpu = new Set([
|
|
573
|
+
GpuRuntimeKind.AwsEc2Gpu,
|
|
574
|
+
GpuRuntimeKind.GcpGceBundled,
|
|
575
|
+
GpuRuntimeKind.GcpGceN1Attached,
|
|
576
|
+
GpuRuntimeKind.AzureVmGpu,
|
|
577
|
+
GpuRuntimeKind.AzureVmVgpu,
|
|
578
|
+
GpuRuntimeKind.LambdaLabs,
|
|
579
|
+
GpuRuntimeKind.CoreWeave,
|
|
580
|
+
]);
|
|
581
|
+
const newEventIds = new Set();
|
|
582
|
+
if (accountant && longRunningGpu.has(accountant.runtime)) {
|
|
583
|
+
const { costDetails, signalEvents } = accountant.snapshotEndAndBuild(durationMs);
|
|
584
|
+
if (costDetails !== null) {
|
|
585
|
+
const ev = createCostEvent({
|
|
586
|
+
eventId: randomUUID(),
|
|
587
|
+
taskId: task.taskId,
|
|
588
|
+
eventType: "gpu_cost",
|
|
589
|
+
costUsd: 0,
|
|
590
|
+
costConfidence: "unknown",
|
|
591
|
+
isRetry: false,
|
|
592
|
+
details: costDetails,
|
|
593
|
+
});
|
|
594
|
+
this._buffer.addEvent(ev);
|
|
595
|
+
this._events.push(ev);
|
|
596
|
+
newEventIds.add(ev.eventId);
|
|
597
|
+
}
|
|
598
|
+
if (signalEvents) {
|
|
599
|
+
for (const sig of signalEvents) {
|
|
600
|
+
const sev = createCostEvent({
|
|
601
|
+
eventId: randomUUID(),
|
|
602
|
+
taskId: task.taskId,
|
|
603
|
+
eventType: "gpu_utilization_signal",
|
|
604
|
+
costUsd: 0, // Decision #3 — observability only
|
|
605
|
+
costConfidence: "unknown",
|
|
606
|
+
isRetry: false,
|
|
607
|
+
details: sig,
|
|
608
|
+
});
|
|
609
|
+
this._buffer.addEvent(sev);
|
|
610
|
+
this._events.push(sev);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// 2. Back-fill cost on every gpu_cost event with cost_pending=true.
|
|
615
|
+
// Per Decision #3, gpu_utilization_signal events are NEVER priced.
|
|
616
|
+
const engine = this._tracker.gpuPricing;
|
|
617
|
+
const events = this._buffer.queryEvents(task.taskId);
|
|
618
|
+
let costDelta = new Decimal(0);
|
|
619
|
+
for (const ev of events) {
|
|
620
|
+
if (ev.eventType !== "gpu_cost")
|
|
621
|
+
continue;
|
|
622
|
+
const details = (ev.details || {});
|
|
623
|
+
if (details.cost_pending !== true)
|
|
624
|
+
continue;
|
|
625
|
+
const oldCost = new Decimal(ev.costUsd);
|
|
626
|
+
const priced = engine.resolveGpuCost(details, cloudEnv, windowS);
|
|
627
|
+
ev.costUsd = priced.costUsd.toNumber();
|
|
628
|
+
ev.pricingSource = priced.pricingSource;
|
|
629
|
+
ev.costConfidence = priced.costConfidence;
|
|
630
|
+
ev.pricingVersion = `gpu:${engine.catalogVersion}`;
|
|
631
|
+
const newDetails = {};
|
|
632
|
+
for (const [k, v] of Object.entries(details)) {
|
|
633
|
+
if (k !== "cost_pending" &&
|
|
634
|
+
k !== "_cgroup_scope_fallback" &&
|
|
635
|
+
k !== "_nvml_product_name_lower") {
|
|
636
|
+
newDetails[k] = v;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
ev.details = newDetails;
|
|
640
|
+
this._buffer.updateEvent(ev);
|
|
641
|
+
const delta = priced.costUsd.minus(oldCost);
|
|
642
|
+
costDelta = costDelta.plus(delta);
|
|
643
|
+
if (newEventIds.has(ev.eventId)) {
|
|
644
|
+
costDelta = costDelta.plus(oldCost); // always 0; explicit
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
const deltaNum = costDelta.toNumber();
|
|
648
|
+
task.gpuCostUsd = decAdd(task.gpuCostUsd, deltaNum);
|
|
649
|
+
task.totalCostUsd = decAdd(task.totalCostUsd, deltaNum);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Snapshot the NetworkAccountant onto the task's v1 fields and (if
|
|
653
|
+
* a CloudEnv has been resolved) compute v2 egress dollars + back-fill
|
|
654
|
+
* the cost_pending network events for this task.
|
|
655
|
+
*
|
|
656
|
+
* Caller (end) wraps this in a Tier-5 fail-silent shell.
|
|
657
|
+
*/
|
|
658
|
+
_finalizeNetwork() {
|
|
659
|
+
// v1 — drain the accountant into task fields. Lookup-then-unregister:
|
|
660
|
+
// late HTTP calls attributed to this task_id won't find an accountant
|
|
661
|
+
// (no orphan rows; matches Python frozen-then-snapshot).
|
|
662
|
+
const accountant = unregisterAccountant(this._task.taskId);
|
|
663
|
+
if (!accountant) {
|
|
664
|
+
// No accountant was registered (ad-hoc task creation outside
|
|
665
|
+
// CostTracker); v1 fields stay at zero.
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const snapshot = accountant.finalize();
|
|
669
|
+
this._task.networkBytesIn = snapshot.bytesIn;
|
|
670
|
+
this._task.networkBytesOut = snapshot.bytesOut;
|
|
671
|
+
this._task.networkCallCount = snapshot.callCount;
|
|
672
|
+
this._task.networkByHost = snapshot.byHost;
|
|
673
|
+
// v2 — egress pricing.
|
|
674
|
+
const env = getCloudEnv();
|
|
675
|
+
const engine = new EgressPricingEngine();
|
|
676
|
+
const rate = engine.resolveRate(env.provider, env.region);
|
|
677
|
+
const pricingVersion = `egress:${engine.catalogVersion}`;
|
|
678
|
+
// Convert external_bytes_out (number) to GB. Per spec §6.3 — 1 GB
|
|
679
|
+
// = 10^9 bytes, NOT 2^30. We use plain `number` here because the TS
|
|
680
|
+
// SDK uses number throughout for cost fields (a pre-existing design
|
|
681
|
+
// choice); the catalog stores rates as strings (preserved exactness
|
|
682
|
+
// at rest) and we parseFloat only at the boundary.
|
|
683
|
+
const ratePerGb = parseFloat(rate.ratePerGb);
|
|
684
|
+
const networkCostUsd = (snapshot.externalBytesOut / 1_000_000_000) * ratePerGb;
|
|
685
|
+
this._task.networkCostUsd = networkCostUsd;
|
|
686
|
+
// Stamp per-host egress_cost_usd into network_by_host[].hosts. The
|
|
687
|
+
// per-host external_bytes_out survives the LIVE_CAP overflow + top-N
|
|
688
|
+
// cap; sum(per-host egress_cost_usd) == network_cost_usd by
|
|
689
|
+
// construction (v2 §10.3 property invariant 2).
|
|
690
|
+
const byHost = this._task.networkByHost;
|
|
691
|
+
if (Array.isArray(byHost.hosts)) {
|
|
692
|
+
for (const host of byHost.hosts) {
|
|
693
|
+
const hostExternal = host["external_bytes_out"] ?? 0;
|
|
694
|
+
const hostCost = (hostExternal / 1_000_000_000) * ratePerGb;
|
|
695
|
+
host["egress_cost_usd"] = String(hostCost);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// v2 §6.4 — back-fill each network event for this task. Walk the
|
|
699
|
+
// buffer's stored events, find any with details.cost_pending ===
|
|
700
|
+
// true, compute their cost, strip the marker, and updateEvent to
|
|
701
|
+
// re-sync.
|
|
702
|
+
const stored = this._buffer.queryEvents(this._task.taskId);
|
|
703
|
+
for (const ev of stored) {
|
|
704
|
+
if (ev.eventType !== "network")
|
|
705
|
+
continue;
|
|
706
|
+
if (ev.details?.cost_pending !== true)
|
|
707
|
+
continue;
|
|
708
|
+
const respBytes = ev.details?.response_bytes ?? 0;
|
|
709
|
+
const reqBytes = ev.details?.request_bytes ?? 0;
|
|
710
|
+
const isInternal = ev.details?.is_internal_traffic === true;
|
|
711
|
+
const billable = isInternal ? 0 : respBytes + reqBytes;
|
|
712
|
+
const evCost = (billable / 1_000_000_000) * ratePerGb;
|
|
713
|
+
ev.costUsd = evCost;
|
|
714
|
+
ev.costConfidence = isInternal
|
|
715
|
+
? "exact"
|
|
716
|
+
: rate.costConfidence;
|
|
717
|
+
ev.pricingVersion = pricingVersion;
|
|
718
|
+
// Strip cost_pending marker so the back-filled event is no longer
|
|
719
|
+
// "deferred-cost".
|
|
720
|
+
delete ev.details.cost_pending;
|
|
721
|
+
// Stamp egress_pricing_source so the wire payload carries the v2
|
|
722
|
+
// source detail (egress_catalog:aws:us-east-1).
|
|
723
|
+
ev.details.egress_pricing_source =
|
|
724
|
+
isInternal ? "egress_catalog:internal" : rate.pricingSource;
|
|
725
|
+
this._buffer.updateEvent(ev);
|
|
726
|
+
// First-pass total_cost_usd summed this event at 0 (cost_pending);
|
|
727
|
+
// add the back-filled cost.
|
|
728
|
+
this._task.totalCostUsd = decAdd(this._task.totalCostUsd, evCost);
|
|
729
|
+
}
|
|
730
|
+
// Add network_cost_usd to total — captures every external byte
|
|
731
|
+
// (cataloged + below-threshold un-cataloged calls included via the
|
|
732
|
+
// accountant scalar even when they emitted no per-event row).
|
|
733
|
+
this._task.totalCostUsd = decAdd(this._task.totalCostUsd, this._task.networkCostUsd);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Record a usage event priced via the rate registry.
|
|
737
|
+
*/
|
|
738
|
+
recordUsage(service, units = 1, details) {
|
|
739
|
+
const rate = this._tracker.getRate(service);
|
|
740
|
+
if (rate === undefined) {
|
|
741
|
+
throw new Error(`No rate registered for service "${service}". Use tracker.registerRate("${service}", per, costUsd) first.`);
|
|
742
|
+
}
|
|
743
|
+
const costUsd = rate * units;
|
|
744
|
+
const event = createCostEvent({
|
|
745
|
+
eventId: randomUUID(),
|
|
746
|
+
taskId: this._task.taskId,
|
|
747
|
+
eventType: "external_cost",
|
|
748
|
+
costUsd,
|
|
749
|
+
costConfidence: "computed",
|
|
750
|
+
pricingSource: "rate_registry",
|
|
751
|
+
pricingVersion: this._tracker.rateRegistry.pricingVersion,
|
|
752
|
+
serviceName: service,
|
|
753
|
+
isRetry: false,
|
|
754
|
+
details: details ?? {},
|
|
755
|
+
});
|
|
756
|
+
this._events.push(event);
|
|
757
|
+
this._buffer.addEvent(event);
|
|
758
|
+
logEvent(event, this._task.taskType);
|
|
759
|
+
this._task.externalCostUsd = decAdd(this._task.externalCostUsd, costUsd);
|
|
760
|
+
this._task.totalCostUsd = decAdd(this._task.totalCostUsd, costUsd);
|
|
761
|
+
this._buffer.upsertTask(this._task);
|
|
762
|
+
return event;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Un-flag a retry event as non-retry, reversing the retry accounting.
|
|
766
|
+
* If eventId is provided, targets that specific event; otherwise targets
|
|
767
|
+
* the most recent retry event.
|
|
768
|
+
*/
|
|
769
|
+
markNotRetry(eventId) {
|
|
770
|
+
let target;
|
|
771
|
+
if (eventId) {
|
|
772
|
+
target = this._events.find((e) => e.eventId === eventId && e.isRetry);
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
for (let i = this._events.length - 1; i >= 0; i--) {
|
|
776
|
+
if (this._events[i].isRetry) {
|
|
777
|
+
target = this._events[i];
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (!target)
|
|
783
|
+
return undefined;
|
|
784
|
+
target.isRetry = false;
|
|
785
|
+
target.retryReason = undefined;
|
|
786
|
+
target.retryOf = undefined;
|
|
787
|
+
this._task.retryCount = Math.max(0, this._task.retryCount - 1);
|
|
788
|
+
this._task.retryCostUsd = Math.max(0, this._task.retryCostUsd - target.costUsd);
|
|
789
|
+
this._buffer.upsertTask(this._task);
|
|
790
|
+
return target;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Main cost tracker for recording AI agent unit economics.
|
|
795
|
+
*
|
|
796
|
+
* Manages task lifecycle, event recording, and background push to
|
|
797
|
+
* a remote endpoint.
|
|
798
|
+
*/
|
|
799
|
+
export class CostTracker {
|
|
800
|
+
_buffer;
|
|
801
|
+
_pusher = null;
|
|
802
|
+
_options;
|
|
803
|
+
_pricing;
|
|
804
|
+
_computePricing;
|
|
805
|
+
_gpuPricing;
|
|
806
|
+
_computeBillingOverrides;
|
|
807
|
+
_k8sNodeAware;
|
|
808
|
+
_rateRegistry;
|
|
809
|
+
_heuristicEngine;
|
|
810
|
+
_instrumented = new Set();
|
|
811
|
+
_config;
|
|
812
|
+
_httpTracked = false;
|
|
813
|
+
constructor(options = {}) {
|
|
814
|
+
this._options = {
|
|
815
|
+
batchSize: 100,
|
|
816
|
+
// Sprint 3 Theme F / §4.1.3 P5: default flush 5 s, matching
|
|
817
|
+
// Python's `flush_interval=5.0`. Pre-fix the TS default was
|
|
818
|
+
// 30 s, leaving up to 6× more time for events to be lost on
|
|
819
|
+
// process exit (and inconsistent with Python's UX).
|
|
820
|
+
flushIntervalMs: 5000,
|
|
821
|
+
...options,
|
|
822
|
+
};
|
|
823
|
+
// Resolve API key (explicit arg → DEXCOST_API_KEY env var) and storage
|
|
824
|
+
// mode. Throws InvalidAPIKeyError for a malformed key.
|
|
825
|
+
this._config = resolveConfig(this._options.apiKey, this._options.storage);
|
|
826
|
+
// Use the resolved key everywhere downstream (env-var fallback included).
|
|
827
|
+
this._options.apiKey = this._config.apiKey;
|
|
828
|
+
this._buffer = new EventBuffer(this._options.dbPath);
|
|
829
|
+
// Dev mode detection
|
|
830
|
+
const env = options?.environment ?? process.env.DEXCOST_ENV;
|
|
831
|
+
if (env === "development") {
|
|
832
|
+
enableDevMode();
|
|
833
|
+
}
|
|
834
|
+
const endpoint = resolveEndpoint();
|
|
835
|
+
const cloudMode = this._config.storageMode === "cloud" && !isDevMode();
|
|
836
|
+
if (cloudMode) {
|
|
837
|
+
this._pusher = new EventPusher(this._buffer, this._options);
|
|
838
|
+
this._pusher.start();
|
|
839
|
+
}
|
|
840
|
+
this._pricing = new PricingEngine();
|
|
841
|
+
this._computePricing = new ComputePricingEngine();
|
|
842
|
+
// GPU pricing engine (Phase 2 — bundled gpu_prices.json). No init knob
|
|
843
|
+
// needed: GPU billing models are unambiguous per provider (Modal is
|
|
844
|
+
// always per_gpu_second_active, etc.). Mirrors python tracker.py.
|
|
845
|
+
this._gpuPricing = new GpuPricingEngine();
|
|
846
|
+
this._computeBillingOverrides = { ...(options.computeBillingOverrides ?? {}) };
|
|
847
|
+
this._k8sNodeAware = options.k8sNodeAware ?? false;
|
|
848
|
+
// Start background pricing refresh in cloud mode
|
|
849
|
+
if (cloudMode && this._config.apiKey) {
|
|
850
|
+
this._pricing.setApiKey(this._config.apiKey);
|
|
851
|
+
this._pricing.startBackgroundRefresh(endpoint);
|
|
852
|
+
}
|
|
853
|
+
this._rateRegistry = new RateRegistry();
|
|
854
|
+
this._heuristicEngine = options.enableRetryHeuristics
|
|
855
|
+
? new RetryHeuristicEngine(options.retryHeuristicWindow, options.retryHeuristicThreshold)
|
|
856
|
+
: null;
|
|
857
|
+
const instruments = options.autoInstrument ?? [...ALL_SUPPORTED_INSTRUMENTS];
|
|
858
|
+
for (const name of instruments) {
|
|
859
|
+
void this.instrument(name);
|
|
860
|
+
}
|
|
861
|
+
// Auto-track outgoing HTTP calls (default on, matches Python).
|
|
862
|
+
if (this._options.trackHttp !== false) {
|
|
863
|
+
void this._enableHttpTracking(this._options.serviceCatalogUrl);
|
|
864
|
+
}
|
|
865
|
+
// Wire the browser adapter to durable storage so trackBrowser() cost
|
|
866
|
+
// events are persisted and shipped by the pusher. Browser tracking is
|
|
867
|
+
// opt-in via the trackBrowser() wrapper (no init flag), so the buffer is
|
|
868
|
+
// wired unconditionally and used only if trackBrowser actually runs.
|
|
869
|
+
void import("../adapters/browser.js").then(({ setBrowserBuffer }) => setBrowserBuffer(this._buffer));
|
|
870
|
+
}
|
|
871
|
+
/** The resolved API-key / storage configuration. */
|
|
872
|
+
get config() {
|
|
873
|
+
return this._config;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Patch outgoing HTTP transports to auto-record external costs and,
|
|
877
|
+
* when a catalog URL is provided, refresh the service catalog.
|
|
878
|
+
*/
|
|
879
|
+
async _enableHttpTracking(serviceCatalogUrl) {
|
|
880
|
+
try {
|
|
881
|
+
const { trackHttp, getServiceCatalog } = await import("../adapters/http.js");
|
|
882
|
+
trackHttp(this._buffer);
|
|
883
|
+
this._httpTracked = true;
|
|
884
|
+
if (serviceCatalogUrl) {
|
|
885
|
+
const catalog = getServiceCatalog();
|
|
886
|
+
if (catalog) {
|
|
887
|
+
await catalog.refreshFromUrl(serviceCatalogUrl);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
// HTTP tracking is best-effort — never crash init.
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/** The underlying event buffer. */
|
|
896
|
+
get buffer() {
|
|
897
|
+
return this._buffer;
|
|
898
|
+
}
|
|
899
|
+
/** The pricing engine used for cost calculations. */
|
|
900
|
+
get pricing() {
|
|
901
|
+
return this._pricing;
|
|
902
|
+
}
|
|
903
|
+
/** The compute pricing engine — wires through to TrackedTask.end finalize. */
|
|
904
|
+
get computePricing() {
|
|
905
|
+
return this._computePricing;
|
|
906
|
+
}
|
|
907
|
+
/** The GPU pricing engine — wires through to TrackedTask.end finalize. */
|
|
908
|
+
get gpuPricing() {
|
|
909
|
+
return this._gpuPricing;
|
|
910
|
+
}
|
|
911
|
+
/** Compute billing-model dispatch overrides (e.g. cloud_run=instance). */
|
|
912
|
+
get computeBillingOverrides() {
|
|
913
|
+
return this._computeBillingOverrides;
|
|
914
|
+
}
|
|
915
|
+
/** Whether K8s node-aware pricing is enabled (reserved for follow-up). */
|
|
916
|
+
get k8sNodeAware() {
|
|
917
|
+
return this._k8sNodeAware;
|
|
918
|
+
}
|
|
919
|
+
/** The rate registry for service-based cost calculations. */
|
|
920
|
+
get rateRegistry() {
|
|
921
|
+
return this._rateRegistry;
|
|
922
|
+
}
|
|
923
|
+
/** The heuristic retry engine, or null if heuristics are disabled. */
|
|
924
|
+
get heuristicEngine() {
|
|
925
|
+
return this._heuristicEngine;
|
|
926
|
+
}
|
|
927
|
+
/** Register a per-unit rate for a named service. */
|
|
928
|
+
registerRate(service, per, costUsd) {
|
|
929
|
+
this._rateRegistry.register(service, per, costUsd);
|
|
930
|
+
}
|
|
931
|
+
/** Get the per-unit cost (in USD) for a named service, or undefined if not registered. */
|
|
932
|
+
getRate(service) {
|
|
933
|
+
return this._rateRegistry.get(service)?.costUsd;
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Activate the named instrument, monkey-patching the provider library.
|
|
937
|
+
*/
|
|
938
|
+
async instrument(name) {
|
|
939
|
+
if (this._instrumented.has(name))
|
|
940
|
+
return;
|
|
941
|
+
const success = await instrumentProvider(name, this._pricing, this._buffer);
|
|
942
|
+
if (success)
|
|
943
|
+
this._instrumented.add(name);
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Deactivate the named instrument, restoring the original library methods.
|
|
947
|
+
*/
|
|
948
|
+
uninstrument(name) {
|
|
949
|
+
uninstrumentProvider(name);
|
|
950
|
+
this._instrumented.delete(name);
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Execute `fn` inside a tracked task context.
|
|
954
|
+
*
|
|
955
|
+
* Creates a new task, runs the function within an AsyncLocalStorage
|
|
956
|
+
* context, and ends the task on completion (or failure).
|
|
957
|
+
*/
|
|
958
|
+
/**
|
|
959
|
+
* Manually start a task and return a `TrackedTask` handle.
|
|
960
|
+
*
|
|
961
|
+
* Use this when callbacks/context managers don't fit your architecture
|
|
962
|
+
* (e.g. Celery-style workers, multi-process pipelines). The caller
|
|
963
|
+
* **must** call `TrackedTask.end()` when the task is complete.
|
|
964
|
+
* Mirrors the Python SDK's `CostTracker.start_task`.
|
|
965
|
+
*/
|
|
966
|
+
startTask(opts = {}) {
|
|
967
|
+
const parentTask = getCurrentTask();
|
|
968
|
+
const task = createTask({
|
|
969
|
+
taskId: randomUUID(),
|
|
970
|
+
taskType: opts.taskType ?? "",
|
|
971
|
+
customerId: opts.customerId,
|
|
972
|
+
projectId: opts.projectId,
|
|
973
|
+
metadata: opts.metadata ? { ...opts.metadata } : {},
|
|
974
|
+
parentTaskId: parentTask?.taskId,
|
|
975
|
+
experimentId: opts.experimentId,
|
|
976
|
+
variant: opts.variant,
|
|
977
|
+
});
|
|
978
|
+
this._buffer.upsertTask(task);
|
|
979
|
+
return new TrackedTask(task, this._buffer, this);
|
|
980
|
+
}
|
|
981
|
+
async track(opts, fn) {
|
|
982
|
+
const parentTask = getCurrentTask();
|
|
983
|
+
const task = createTask({
|
|
984
|
+
taskId: randomUUID(),
|
|
985
|
+
taskType: opts.taskType,
|
|
986
|
+
customerId: opts.customerId,
|
|
987
|
+
projectId: opts.projectId,
|
|
988
|
+
metadata: opts.metadata ? { ...opts.metadata } : {},
|
|
989
|
+
parentTaskId: parentTask?.taskId,
|
|
990
|
+
experimentId: opts.experimentId,
|
|
991
|
+
variant: opts.variant,
|
|
992
|
+
});
|
|
993
|
+
this._buffer.upsertTask(task);
|
|
994
|
+
const trackedTask = new TrackedTask(task, this._buffer, this);
|
|
995
|
+
try {
|
|
996
|
+
const result = await runWithTask(task, () => fn(trackedTask));
|
|
997
|
+
if (task.status === "pending") {
|
|
998
|
+
trackedTask.end("success");
|
|
999
|
+
}
|
|
1000
|
+
return result;
|
|
1001
|
+
}
|
|
1002
|
+
catch (error) {
|
|
1003
|
+
trackedTask.end("failed");
|
|
1004
|
+
throw error;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Force an immediate flush of all buffered events to the remote endpoint.
|
|
1009
|
+
*/
|
|
1010
|
+
async flush() {
|
|
1011
|
+
if (this._pusher) {
|
|
1012
|
+
await this._pusher.flush();
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Update the API key on both pricing engine and pusher. Sprint 2
|
|
1017
|
+
* Theme D / §3.2.3 (B14) — entry point for `dexcost.setApiKey`.
|
|
1018
|
+
*/
|
|
1019
|
+
setApiKey(newKey) {
|
|
1020
|
+
this._config = { ...this._config, apiKey: newKey };
|
|
1021
|
+
this._pricing.setApiKey(newKey);
|
|
1022
|
+
if (this._pusher) {
|
|
1023
|
+
this._pusher.setApiKey(newKey);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Stop the background pusher and release resources.
|
|
1028
|
+
*/
|
|
1029
|
+
close() {
|
|
1030
|
+
for (const name of this._instrumented) {
|
|
1031
|
+
uninstrumentProvider(name);
|
|
1032
|
+
}
|
|
1033
|
+
this._instrumented.clear();
|
|
1034
|
+
this._disableHttpTracking();
|
|
1035
|
+
if (this._pusher) {
|
|
1036
|
+
// Note: flush() is async but close() is sync by contract.
|
|
1037
|
+
// We call stop() which clears the interval; any in-flight push
|
|
1038
|
+
// completes naturally. Use flush() before close() for guaranteed delivery.
|
|
1039
|
+
this._pusher.stop();
|
|
1040
|
+
}
|
|
1041
|
+
this._pricing.stopBackgroundRefresh();
|
|
1042
|
+
this._buffer.close();
|
|
1043
|
+
}
|
|
1044
|
+
/** Restore patched HTTP transports if HTTP tracking was enabled. */
|
|
1045
|
+
_disableHttpTracking() {
|
|
1046
|
+
if (!this._httpTracked)
|
|
1047
|
+
return;
|
|
1048
|
+
this._httpTracked = false;
|
|
1049
|
+
void import("../adapters/http.js")
|
|
1050
|
+
.then(({ untrackHttp }) => untrackHttp())
|
|
1051
|
+
.catch(() => {
|
|
1052
|
+
// best-effort
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Flush pending events and then stop the background pusher and release resources.
|
|
1057
|
+
* Prefer this over close() when you need to guarantee all events are delivered.
|
|
1058
|
+
*/
|
|
1059
|
+
async closeAsync() {
|
|
1060
|
+
for (const name of this._instrumented) {
|
|
1061
|
+
uninstrumentProvider(name);
|
|
1062
|
+
}
|
|
1063
|
+
this._instrumented.clear();
|
|
1064
|
+
this._disableHttpTracking();
|
|
1065
|
+
if (this._pusher) {
|
|
1066
|
+
await this._pusher.flush();
|
|
1067
|
+
this._pusher.stop();
|
|
1068
|
+
}
|
|
1069
|
+
this._pricing.stopBackgroundRefresh();
|
|
1070
|
+
this._buffer.close();
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
//# sourceMappingURL=tracker.js.map
|