@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.
Files changed (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/dist/adapters/_netbytes.d.ts +31 -0
  4. package/dist/adapters/_netbytes.d.ts.map +1 -0
  5. package/dist/adapters/_netbytes.js +154 -0
  6. package/dist/adapters/_netbytes.js.map +1 -0
  7. package/dist/adapters/aws-lambda.d.ts +41 -0
  8. package/dist/adapters/aws-lambda.d.ts.map +1 -0
  9. package/dist/adapters/aws-lambda.js +65 -0
  10. package/dist/adapters/aws-lambda.js.map +1 -0
  11. package/dist/adapters/browser.d.ts +52 -0
  12. package/dist/adapters/browser.d.ts.map +1 -0
  13. package/dist/adapters/browser.js +127 -0
  14. package/dist/adapters/browser.js.map +1 -0
  15. package/dist/adapters/compute-wrap.d.ts +33 -0
  16. package/dist/adapters/compute-wrap.d.ts.map +1 -0
  17. package/dist/adapters/compute-wrap.js +188 -0
  18. package/dist/adapters/compute-wrap.js.map +1 -0
  19. package/dist/adapters/data/aws_lambda_pricing.json +61 -0
  20. package/dist/adapters/gpu-wrap.d.ts +31 -0
  21. package/dist/adapters/gpu-wrap.d.ts.map +1 -0
  22. package/dist/adapters/gpu-wrap.js +147 -0
  23. package/dist/adapters/gpu-wrap.js.map +1 -0
  24. package/dist/adapters/http.d.ts +58 -0
  25. package/dist/adapters/http.d.ts.map +1 -0
  26. package/dist/adapters/http.js +769 -0
  27. package/dist/adapters/http.js.map +1 -0
  28. package/dist/adapters/index.d.ts +11 -0
  29. package/dist/adapters/index.d.ts.map +1 -0
  30. package/dist/adapters/index.js +12 -0
  31. package/dist/adapters/index.js.map +1 -0
  32. package/dist/adapters/network-accountant.d.ts +63 -0
  33. package/dist/adapters/network-accountant.d.ts.map +1 -0
  34. package/dist/adapters/network-accountant.js +153 -0
  35. package/dist/adapters/network-accountant.js.map +1 -0
  36. package/dist/cli/index.d.ts +13 -0
  37. package/dist/cli/index.d.ts.map +1 -0
  38. package/dist/cli/index.js +225 -0
  39. package/dist/cli/index.js.map +1 -0
  40. package/dist/cli/scanner.d.ts +39 -0
  41. package/dist/cli/scanner.d.ts.map +1 -0
  42. package/dist/cli/scanner.js +480 -0
  43. package/dist/cli/scanner.js.map +1 -0
  44. package/dist/clients.d.ts +54 -0
  45. package/dist/clients.d.ts.map +1 -0
  46. package/dist/clients.js +240 -0
  47. package/dist/clients.js.map +1 -0
  48. package/dist/cloud-detect.d.ts +96 -0
  49. package/dist/cloud-detect.d.ts.map +1 -0
  50. package/dist/cloud-detect.js +545 -0
  51. package/dist/cloud-detect.js.map +1 -0
  52. package/dist/core/auto-task.d.ts +20 -0
  53. package/dist/core/auto-task.d.ts.map +1 -0
  54. package/dist/core/auto-task.js +34 -0
  55. package/dist/core/auto-task.js.map +1 -0
  56. package/dist/core/cgroup-reader.d.ts +45 -0
  57. package/dist/core/cgroup-reader.d.ts.map +1 -0
  58. package/dist/core/cgroup-reader.js +124 -0
  59. package/dist/core/cgroup-reader.js.map +1 -0
  60. package/dist/core/cgroup-walker.d.ts +60 -0
  61. package/dist/core/cgroup-walker.d.ts.map +1 -0
  62. package/dist/core/cgroup-walker.js +166 -0
  63. package/dist/core/cgroup-walker.js.map +1 -0
  64. package/dist/core/compute-accountant.d.ts +51 -0
  65. package/dist/core/compute-accountant.d.ts.map +1 -0
  66. package/dist/core/compute-accountant.js +179 -0
  67. package/dist/core/compute-accountant.js.map +1 -0
  68. package/dist/core/compute-runtime.d.ts +42 -0
  69. package/dist/core/compute-runtime.d.ts.map +1 -0
  70. package/dist/core/compute-runtime.js +80 -0
  71. package/dist/core/compute-runtime.js.map +1 -0
  72. package/dist/core/config.d.ts +44 -0
  73. package/dist/core/config.d.ts.map +1 -0
  74. package/dist/core/config.js +66 -0
  75. package/dist/core/config.js.map +1 -0
  76. package/dist/core/context.d.ts +76 -0
  77. package/dist/core/context.d.ts.map +1 -0
  78. package/dist/core/context.js +91 -0
  79. package/dist/core/context.js.map +1 -0
  80. package/dist/core/fargate-metadata.d.ts +27 -0
  81. package/dist/core/fargate-metadata.d.ts.map +1 -0
  82. package/dist/core/fargate-metadata.js +102 -0
  83. package/dist/core/fargate-metadata.js.map +1 -0
  84. package/dist/core/gpu-accountant.d.ts +104 -0
  85. package/dist/core/gpu-accountant.d.ts.map +1 -0
  86. package/dist/core/gpu-accountant.js +383 -0
  87. package/dist/core/gpu-accountant.js.map +1 -0
  88. package/dist/core/gpu-runtime.d.ts +58 -0
  89. package/dist/core/gpu-runtime.d.ts.map +1 -0
  90. package/dist/core/gpu-runtime.js +131 -0
  91. package/dist/core/gpu-runtime.js.map +1 -0
  92. package/dist/core/heuristics.d.ts +74 -0
  93. package/dist/core/heuristics.d.ts.map +1 -0
  94. package/dist/core/heuristics.js +182 -0
  95. package/dist/core/heuristics.js.map +1 -0
  96. package/dist/core/models.d.ts +149 -0
  97. package/dist/core/models.d.ts.map +1 -0
  98. package/dist/core/models.js +226 -0
  99. package/dist/core/models.js.map +1 -0
  100. package/dist/core/nvml-reader.d.ts +114 -0
  101. package/dist/core/nvml-reader.d.ts.map +1 -0
  102. package/dist/core/nvml-reader.js +323 -0
  103. package/dist/core/nvml-reader.js.map +1 -0
  104. package/dist/core/session.d.ts +48 -0
  105. package/dist/core/session.d.ts.map +1 -0
  106. package/dist/core/session.js +123 -0
  107. package/dist/core/session.js.map +1 -0
  108. package/dist/core/tracker.d.ts +364 -0
  109. package/dist/core/tracker.d.ts.map +1 -0
  110. package/dist/core/tracker.js +1073 -0
  111. package/dist/core/tracker.js.map +1 -0
  112. package/dist/data/compute_prices.json +180 -0
  113. package/dist/data/egress_prices.json +418 -0
  114. package/dist/data/gpu_prices.json +412 -0
  115. package/dist/data/service_prices.json +2595 -0
  116. package/dist/dev-console.d.ts +12 -0
  117. package/dist/dev-console.d.ts.map +1 -0
  118. package/dist/dev-console.js +60 -0
  119. package/dist/dev-console.js.map +1 -0
  120. package/dist/index.d.ts +52 -0
  121. package/dist/index.d.ts.map +1 -0
  122. package/dist/index.js +61 -0
  123. package/dist/index.js.map +1 -0
  124. package/dist/instruments/anthropic.d.ts +26 -0
  125. package/dist/instruments/anthropic.d.ts.map +1 -0
  126. package/dist/instruments/anthropic.js +242 -0
  127. package/dist/instruments/anthropic.js.map +1 -0
  128. package/dist/instruments/bedrock.d.ts +29 -0
  129. package/dist/instruments/bedrock.d.ts.map +1 -0
  130. package/dist/instruments/bedrock.js +215 -0
  131. package/dist/instruments/bedrock.js.map +1 -0
  132. package/dist/instruments/cohere.d.ts +29 -0
  133. package/dist/instruments/cohere.d.ts.map +1 -0
  134. package/dist/instruments/cohere.js +237 -0
  135. package/dist/instruments/cohere.js.map +1 -0
  136. package/dist/instruments/gemini.d.ts +30 -0
  137. package/dist/instruments/gemini.d.ts.map +1 -0
  138. package/dist/instruments/gemini.js +247 -0
  139. package/dist/instruments/gemini.js.map +1 -0
  140. package/dist/instruments/index.d.ts +35 -0
  141. package/dist/instruments/index.d.ts.map +1 -0
  142. package/dist/instruments/index.js +54 -0
  143. package/dist/instruments/index.js.map +1 -0
  144. package/dist/instruments/mcp.d.ts +24 -0
  145. package/dist/instruments/mcp.d.ts.map +1 -0
  146. package/dist/instruments/mcp.js +459 -0
  147. package/dist/instruments/mcp.js.map +1 -0
  148. package/dist/instruments/openai.d.ts +26 -0
  149. package/dist/instruments/openai.d.ts.map +1 -0
  150. package/dist/instruments/openai.js +221 -0
  151. package/dist/instruments/openai.js.map +1 -0
  152. package/dist/instruments/vercel-ai.d.ts +28 -0
  153. package/dist/instruments/vercel-ai.d.ts.map +1 -0
  154. package/dist/instruments/vercel-ai.js +192 -0
  155. package/dist/instruments/vercel-ai.js.map +1 -0
  156. package/dist/integrations/langchain.d.ts +65 -0
  157. package/dist/integrations/langchain.d.ts.map +1 -0
  158. package/dist/integrations/langchain.js +165 -0
  159. package/dist/integrations/langchain.js.map +1 -0
  160. package/dist/middleware/express.d.ts +55 -0
  161. package/dist/middleware/express.d.ts.map +1 -0
  162. package/dist/middleware/express.js +101 -0
  163. package/dist/middleware/express.js.map +1 -0
  164. package/dist/middleware/index.d.ts +6 -0
  165. package/dist/middleware/index.d.ts.map +1 -0
  166. package/dist/middleware/index.js +5 -0
  167. package/dist/middleware/index.js.map +1 -0
  168. package/dist/pricing/compute-pricing.d.ts +57 -0
  169. package/dist/pricing/compute-pricing.d.ts.map +1 -0
  170. package/dist/pricing/compute-pricing.js +627 -0
  171. package/dist/pricing/compute-pricing.js.map +1 -0
  172. package/dist/pricing/cost_map.json +37665 -0
  173. package/dist/pricing/egress-pricing.d.ts +55 -0
  174. package/dist/pricing/egress-pricing.d.ts.map +1 -0
  175. package/dist/pricing/egress-pricing.js +226 -0
  176. package/dist/pricing/egress-pricing.js.map +1 -0
  177. package/dist/pricing/engine.d.ts +24 -0
  178. package/dist/pricing/engine.d.ts.map +1 -0
  179. package/dist/pricing/engine.js +148 -0
  180. package/dist/pricing/engine.js.map +1 -0
  181. package/dist/pricing/gpu-pricing.d.ts +63 -0
  182. package/dist/pricing/gpu-pricing.d.ts.map +1 -0
  183. package/dist/pricing/gpu-pricing.js +484 -0
  184. package/dist/pricing/gpu-pricing.js.map +1 -0
  185. package/dist/pricing/rates.d.ts +17 -0
  186. package/dist/pricing/rates.d.ts.map +1 -0
  187. package/dist/pricing/rates.js +102 -0
  188. package/dist/pricing/rates.js.map +1 -0
  189. package/dist/pricing/service-catalog.d.ts +87 -0
  190. package/dist/pricing/service-catalog.d.ts.map +1 -0
  191. package/dist/pricing/service-catalog.js +406 -0
  192. package/dist/pricing/service-catalog.js.map +1 -0
  193. package/dist/schema/dexcost-event.v1.json +111 -0
  194. package/dist/schema/dexcost-task.v1.json +160 -0
  195. package/dist/schema/validate.d.ts +15 -0
  196. package/dist/schema/validate.d.ts.map +1 -0
  197. package/dist/schema/validate.js +87 -0
  198. package/dist/schema/validate.js.map +1 -0
  199. package/dist/security/redaction.d.ts +55 -0
  200. package/dist/security/redaction.d.ts.map +1 -0
  201. package/dist/security/redaction.js +144 -0
  202. package/dist/security/redaction.js.map +1 -0
  203. package/dist/transport/buffer.d.ts +117 -0
  204. package/dist/transport/buffer.d.ts.map +1 -0
  205. package/dist/transport/buffer.js +759 -0
  206. package/dist/transport/buffer.js.map +1 -0
  207. package/dist/transport/pusher.d.ts +89 -0
  208. package/dist/transport/pusher.d.ts.map +1 -0
  209. package/dist/transport/pusher.js +323 -0
  210. package/dist/transport/pusher.js.map +1 -0
  211. package/package.json +93 -0
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Core data models for dexcost TypeScript SDK.
3
+ *
4
+ * These interfaces match the Dexcost Standard Event Schema v1
5
+ * and mirror the Python SDK's Task and Event dataclasses.
6
+ */
7
+ /**
8
+ * Serialise a Date to the canonical wire format. Sprint 3 Theme F /
9
+ * §4.1.1 (P1): RFC3339 with microsecond precision (6 fractional
10
+ * digits) + "Z" suffix, matching the Python canonical.
11
+ *
12
+ * `Date.toISOString()` only gives 3-digit millisecond precision, so
13
+ * we pad with three trailing zeros. Customer-visible Date objects
14
+ * carry no sub-millisecond data anyway, so the pad is information-
15
+ * preserving — it just aligns the wire string for cross-SDK parity.
16
+ */
17
+ function isoCanonical(d) {
18
+ const iso = d.toISOString();
19
+ // iso is `YYYY-MM-DDTHH:mm:ss.sssZ` (24 chars); pad to .sssNNNZ
20
+ return iso.slice(0, -1) + "000Z";
21
+ }
22
+ /** Create a default Task with required fields. */
23
+ export function createTask(overrides) {
24
+ return {
25
+ taskType: "",
26
+ status: "pending",
27
+ startedAt: new Date(),
28
+ metadata: {},
29
+ llmCostUsd: 0,
30
+ externalCostUsd: 0,
31
+ computeCostUsd: 0,
32
+ networkCostUsd: 0,
33
+ gpuCostUsd: 0,
34
+ totalCostUsd: 0,
35
+ totalInputTokens: 0,
36
+ totalOutputTokens: 0,
37
+ totalCachedTokens: 0,
38
+ retryCount: 0,
39
+ retryCostUsd: 0,
40
+ failureCount: 0,
41
+ networkBytesIn: 0,
42
+ networkBytesOut: 0,
43
+ networkCallCount: 0,
44
+ networkByHost: { hosts: [] },
45
+ schemaVersion: "1",
46
+ ...overrides,
47
+ };
48
+ }
49
+ /** Create a default CostEvent with required fields. */
50
+ export function createCostEvent(overrides) {
51
+ return {
52
+ eventType: "llm_call",
53
+ occurredAt: new Date(),
54
+ costUsd: 0,
55
+ costConfidence: "exact",
56
+ isRetry: false,
57
+ details: {},
58
+ schemaVersion: "1",
59
+ ...overrides,
60
+ };
61
+ }
62
+ /**
63
+ * Serialise a Task to a JSON-safe dictionary matching the Standard Event Schema v1.
64
+ * Costs are serialised as strings to preserve precision.
65
+ */
66
+ export function taskToDict(task) {
67
+ return {
68
+ task_id: task.taskId,
69
+ task_type: task.taskType,
70
+ status: task.status,
71
+ started_at: isoCanonical(task.startedAt),
72
+ ended_at: task.endedAt ? isoCanonical(task.endedAt) : null,
73
+ metadata: task.metadata,
74
+ customer_id: task.customerId ?? null,
75
+ project_id: task.projectId ?? null,
76
+ parent_task_id: task.parentTaskId ?? null,
77
+ experiment_id: task.experimentId ?? null,
78
+ variant: task.variant ?? null,
79
+ llm_cost_usd: String(task.llmCostUsd),
80
+ external_cost_usd: String(task.externalCostUsd),
81
+ compute_cost_usd: String(task.computeCostUsd),
82
+ network_cost_usd: String(task.networkCostUsd),
83
+ gpu_cost_usd: String(task.gpuCostUsd),
84
+ total_cost_usd: String(task.totalCostUsd),
85
+ total_input_tokens: task.totalInputTokens,
86
+ total_output_tokens: task.totalOutputTokens,
87
+ total_cached_tokens: task.totalCachedTokens,
88
+ retry_count: task.retryCount,
89
+ retry_cost_usd: String(task.retryCostUsd),
90
+ failure_count: task.failureCount,
91
+ network_bytes_in: task.networkBytesIn,
92
+ network_bytes_out: task.networkBytesOut,
93
+ network_call_count: task.networkCallCount,
94
+ network_by_host: task.networkByHost,
95
+ schema_version: task.schemaVersion,
96
+ };
97
+ }
98
+ /**
99
+ * Serialise a CostEvent to a JSON-safe dictionary matching the Standard Event Schema v1.
100
+ * Costs are serialised as strings to preserve precision.
101
+ */
102
+ export function eventToDict(event) {
103
+ return {
104
+ event_id: event.eventId,
105
+ task_id: event.taskId,
106
+ event_type: event.eventType,
107
+ occurred_at: isoCanonical(event.occurredAt),
108
+ cost_usd: String(event.costUsd),
109
+ cost_confidence: event.costConfidence,
110
+ pricing_source: event.pricingSource ?? null,
111
+ pricing_version: event.pricingVersion ?? null,
112
+ provider: event.provider ?? null,
113
+ model: event.model ?? null,
114
+ input_tokens: event.inputTokens ?? null,
115
+ output_tokens: event.outputTokens ?? null,
116
+ cached_tokens: event.cachedTokens ?? null,
117
+ latency_ms: event.latencyMs ?? null,
118
+ service_name: event.serviceName ?? null,
119
+ is_retry: event.isRetry,
120
+ retry_reason: event.retryReason ?? null,
121
+ retry_of: event.retryOf ?? null,
122
+ details: event.details,
123
+ schema_version: event.schemaVersion,
124
+ };
125
+ }
126
+ // ---------------------------------------------------------------------------
127
+ // Deserialisation helpers (inverse of taskToDict / eventToDict)
128
+ // ---------------------------------------------------------------------------
129
+ function _requireString(data, key) {
130
+ const value = data[key];
131
+ if (typeof value !== "string") {
132
+ throw new Error(`Invalid task/event data: missing or non-string field "${key}"`);
133
+ }
134
+ return value;
135
+ }
136
+ function _optString(value) {
137
+ return typeof value === "string" ? value : undefined;
138
+ }
139
+ function _toNumber(value, fallback = 0) {
140
+ if (typeof value === "number")
141
+ return value;
142
+ if (typeof value === "string" && value.trim() !== "") {
143
+ const n = Number(value);
144
+ if (!Number.isNaN(n))
145
+ return n;
146
+ }
147
+ return fallback;
148
+ }
149
+ /**
150
+ * Deserialise a Task from a JSON-safe dictionary (inverse of `taskToDict`).
151
+ *
152
+ * Mirrors the Python SDK's `Task.from_dict`. Throws an Error when required
153
+ * fields are missing or malformed.
154
+ */
155
+ export function taskFromDict(data) {
156
+ const startedAtRaw = _requireString(data, "started_at");
157
+ const endedAtRaw = data["ended_at"];
158
+ return {
159
+ taskId: _requireString(data, "task_id"),
160
+ taskType: typeof data["task_type"] === "string" ? data["task_type"] : "",
161
+ status: _requireString(data, "status"),
162
+ startedAt: new Date(startedAtRaw),
163
+ endedAt: typeof endedAtRaw === "string" ? new Date(endedAtRaw) : undefined,
164
+ metadata: data["metadata"] && typeof data["metadata"] === "object"
165
+ ? data["metadata"]
166
+ : {},
167
+ customerId: _optString(data["customer_id"]),
168
+ projectId: _optString(data["project_id"]),
169
+ parentTaskId: _optString(data["parent_task_id"]),
170
+ experimentId: _optString(data["experiment_id"]),
171
+ variant: _optString(data["variant"]),
172
+ llmCostUsd: _toNumber(data["llm_cost_usd"]),
173
+ externalCostUsd: _toNumber(data["external_cost_usd"]),
174
+ computeCostUsd: _toNumber(data["compute_cost_usd"]),
175
+ networkCostUsd: _toNumber(data["network_cost_usd"]),
176
+ gpuCostUsd: _toNumber(data["gpu_cost_usd"]),
177
+ totalCostUsd: _toNumber(data["total_cost_usd"]),
178
+ totalInputTokens: _toNumber(data["total_input_tokens"]),
179
+ totalOutputTokens: _toNumber(data["total_output_tokens"]),
180
+ totalCachedTokens: _toNumber(data["total_cached_tokens"]),
181
+ retryCount: _toNumber(data["retry_count"]),
182
+ retryCostUsd: _toNumber(data["retry_cost_usd"]),
183
+ failureCount: _toNumber(data["failure_count"]),
184
+ networkBytesIn: _toNumber(data["network_bytes_in"]),
185
+ networkBytesOut: _toNumber(data["network_bytes_out"]),
186
+ networkCallCount: _toNumber(data["network_call_count"]),
187
+ networkByHost: data["network_by_host"] && typeof data["network_by_host"] === "object"
188
+ ? data["network_by_host"]
189
+ : { hosts: [] },
190
+ schemaVersion: _optString(data["schema_version"]) ?? "1",
191
+ };
192
+ }
193
+ /**
194
+ * Deserialise a CostEvent from a JSON-safe dictionary (inverse of `eventToDict`).
195
+ *
196
+ * Mirrors the Python SDK's `Event.from_dict`. Throws an Error when required
197
+ * fields are missing or malformed.
198
+ */
199
+ export function eventFromDict(data) {
200
+ const occurredAtRaw = _requireString(data, "occurred_at");
201
+ return {
202
+ eventId: _requireString(data, "event_id"),
203
+ taskId: _requireString(data, "task_id"),
204
+ eventType: _requireString(data, "event_type"),
205
+ occurredAt: new Date(occurredAtRaw),
206
+ costUsd: _toNumber(data["cost_usd"]),
207
+ costConfidence: _requireString(data, "cost_confidence"),
208
+ pricingSource: _optString(data["pricing_source"]),
209
+ pricingVersion: _optString(data["pricing_version"]),
210
+ provider: _optString(data["provider"]),
211
+ model: _optString(data["model"]),
212
+ inputTokens: typeof data["input_tokens"] === "number" ? data["input_tokens"] : undefined,
213
+ outputTokens: typeof data["output_tokens"] === "number" ? data["output_tokens"] : undefined,
214
+ cachedTokens: typeof data["cached_tokens"] === "number" ? data["cached_tokens"] : undefined,
215
+ latencyMs: typeof data["latency_ms"] === "number" ? data["latency_ms"] : undefined,
216
+ serviceName: _optString(data["service_name"]),
217
+ isRetry: data["is_retry"] === true,
218
+ retryReason: _optString(data["retry_reason"]),
219
+ retryOf: _optString(data["retry_of"]),
220
+ details: data["details"] && typeof data["details"] === "object"
221
+ ? data["details"]
222
+ : {},
223
+ schemaVersion: _optString(data["schema_version"]) ?? "1",
224
+ };
225
+ }
226
+ //# sourceMappingURL=models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"models.js","sourceRoot":"","sources":["../../src/core/models.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * NVML reader — Phase 2 GPU foundation.
3
+ *
4
+ * **TypeScript architectural deviation from Python**: there is no maintained
5
+ * native NVML binding for Node in 2026. This module shells out to the
6
+ * `nvidia-smi` CLI via `child_process.spawnSync` and parses CSV output.
7
+ * Functions still return typed objects (ProcessInfo, UtilSample, MemInfo)
8
+ * so downstream callers see the same shape as the Python `pynvml`
9
+ * wrapper. Per-probe overhead is ~50ms (acceptable for finalize-time use;
10
+ * not suitable for hot-path sampling — but neither is NVML).
11
+ *
12
+ * Fail-silent contract (convention §9) identical to Python:
13
+ * every reader returns `null` (or `false` for booleans) on missing binary,
14
+ * non-zero exit, or parse failure — the caller (GpuAccountant) decides
15
+ * the fallback policy per Decision #1's classification table.
16
+ *
17
+ * **Browser safety**: nvmlAvailable() short-circuits to false off-Node.
18
+ *
19
+ * Decision #4: getProductName applies NFC + lowercase + whitespace collapse
20
+ * on the raw nvidia-smi product-name output before returning — catalog
21
+ * alias matching depends on byte-level equality after normalization.
22
+ *
23
+ * Decision #8: getProcessUtilization takes a mutable `lastSeenTimestamps`
24
+ * dict and updates it in place. The TS implementation uses
25
+ * `process.hrtime.bigint() → ms` for the per-PID timestamp (nvidia-smi pmon
26
+ * does not expose NVML's sample-buffer timestamp). The accountant uses
27
+ * (endTs - startTs) ms as the per-PID active window proxy.
28
+ */
29
+ import type { SpawnSyncReturns } from "node:child_process";
30
+ type SpawnFn = (cmd: string, args: string[], options: {
31
+ encoding: "utf-8";
32
+ timeout: number;
33
+ }) => SpawnSyncReturns<string>;
34
+ /** Test-only — override the spawnSync implementation. Pass `null` to restore. */
35
+ export declare function _setSpawnFnForTests(fn: SpawnFn | null): void;
36
+ export declare function _resetWarningStateForTests(): void;
37
+ /** Test-only — override the nvidia-smi binary path. Pass `null` to restore. */
38
+ export declare function _setNvidiaSmiPathForTests(p: string | null): void;
39
+ export interface ProcessInfo {
40
+ /** PID of the process holding the GPU. */
41
+ pid: number;
42
+ /** VRAM used by this process in bytes (0 when nvidia-smi reports N/A). */
43
+ usedGpuMemory: number;
44
+ }
45
+ export interface UtilSample {
46
+ pid: number;
47
+ /** 0–100; percent of time SMs had ≥1 kernel running during the sample window. */
48
+ smUtil: number;
49
+ /** 0–100; percent of time memory subsystem was busy. */
50
+ memUtil: number;
51
+ /** Per-PID timestamp (milliseconds since process start) for Decision #8 state. */
52
+ timeStamp: number;
53
+ }
54
+ export interface MemInfo {
55
+ usedBytes: number;
56
+ totalBytes: number;
57
+ }
58
+ /**
59
+ * True when `nvidia-smi` is invokable and exits 0 on the count query.
60
+ *
61
+ * The TS-SDK definition of "available" is "binary works"; in Python this
62
+ * was "pynvml importable AND nvmlInit succeeds". Same end behaviour:
63
+ * downstream code checks this predicate before calling readers.
64
+ */
65
+ export declare function nvmlAvailable(): boolean;
66
+ /** Call `nvidia-smi --query-gpu=count` once. True on success, false on any failure. */
67
+ export declare function initNvml(): boolean;
68
+ /** No-op on TS; the CLI has no persistent handle. Provided for API parity. */
69
+ export declare function shutdownNvml(): void;
70
+ /** Number of NVIDIA devices visible to nvidia-smi. `null` on failure. */
71
+ export declare function getDeviceCount(): number | null;
72
+ /**
73
+ * Return the nvidia-smi product-name for `index`, NFC-normalized + lowercased.
74
+ *
75
+ * Decision #4 — alias matching against the catalog depends on byte-level
76
+ * equality post-normalization.
77
+ */
78
+ export declare function getProductName(index: number): string | null;
79
+ /**
80
+ * List of PIDs currently holding GPU `index` + their VRAM usage in bytes.
81
+ *
82
+ * Returns `null` on permission denied / failure — the Decision #1
83
+ * load-bearing case for non-root containers. The accountant's cgroup walk
84
+ * then degrades to self-PID-only.
85
+ */
86
+ export declare function getComputeRunningProcesses(index: number): ProcessInfo[] | null;
87
+ /**
88
+ * Per-PID utilization sampled from `nvidia-smi pmon -c 1 -s u`.
89
+ *
90
+ * Decision #8 — TS uses `Date.now()` as the per-PID timestamp proxy
91
+ * because nvidia-smi pmon does not expose the NVML sample-buffer epoch.
92
+ * The accountant uses (endTs - startTs) ms as the active-GPU-time
93
+ * approximation between snapshots.
94
+ *
95
+ * Updates `lastSeenTimestamps` in place — callers persist across snapshots.
96
+ */
97
+ /**
98
+ * Returns multi-sample-per-PID utilization data.
99
+ *
100
+ * Sprint 2 Theme C / §3.1.1 (B2 TS port). Pre-fix the return type
101
+ * was `Record<number, UtilSample>` — single-latest-sample-per-PID,
102
+ * collapsing the integration data the accountant needs. Now returns
103
+ * `Record<number, UtilSample[]>` with samples sorted by `timeStamp`
104
+ * ascending. nvidia-smi pmon returns one snapshot per call, so the
105
+ * array length is 1 in practice; the type accommodates future
106
+ * sampling implementations that batch multiple observations.
107
+ */
108
+ export declare function getProcessUtilization(index: number, lastSeenTimestamps: Record<number, number>): Record<number, UtilSample[]> | null;
109
+ /** Device-level used / total VRAM (bytes). `null` on failure. */
110
+ export declare function getMemoryInfo(index: number): MemInfo | null;
111
+ /** True when MIG is enabled on this device (Decision #2 detection). */
112
+ export declare function getMigMode(index: number): boolean;
113
+ export {};
114
+ //# sourceMappingURL=nvml-reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nvml-reader.d.ts","sourceRoot":"","sources":["../../src/core/nvml-reader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAI3D,KAAK,OAAO,GAAG,CACb,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,KAC5C,gBAAgB,CAAC,MAAM,CAAC,CAAC;AAK9B,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAK5D;AAMD,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD;AAmBD,+EAA+E;AAC/E,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEhE;AAID,MAAM,WAAW,WAAW;IAC1B,0CAA0C;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,OAAO;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AA6BD;;;;;;GAMG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAYvC;AAED,uFAAuF;AACvF,wBAAgB,QAAQ,IAAI,OAAO,CAElC;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,IAAI,IAAI,CAEnC;AAID,yEAAyE;AACzE,wBAAgB,cAAc,IAAI,MAAM,GAAG,IAAI,CAc9C;AAID;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB3D;AAID;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE,GAAG,IAAI,CA8B9E;AAID;;;;;;;;;GASG;AACH;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,EACb,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACzC,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,GAAG,IAAI,CAmDrC;AAID,iEAAiE;AACjE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CA0B3D;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAejD"}
@@ -0,0 +1,323 @@
1
+ /**
2
+ * NVML reader — Phase 2 GPU foundation.
3
+ *
4
+ * **TypeScript architectural deviation from Python**: there is no maintained
5
+ * native NVML binding for Node in 2026. This module shells out to the
6
+ * `nvidia-smi` CLI via `child_process.spawnSync` and parses CSV output.
7
+ * Functions still return typed objects (ProcessInfo, UtilSample, MemInfo)
8
+ * so downstream callers see the same shape as the Python `pynvml`
9
+ * wrapper. Per-probe overhead is ~50ms (acceptable for finalize-time use;
10
+ * not suitable for hot-path sampling — but neither is NVML).
11
+ *
12
+ * Fail-silent contract (convention §9) identical to Python:
13
+ * every reader returns `null` (or `false` for booleans) on missing binary,
14
+ * non-zero exit, or parse failure — the caller (GpuAccountant) decides
15
+ * the fallback policy per Decision #1's classification table.
16
+ *
17
+ * **Browser safety**: nvmlAvailable() short-circuits to false off-Node.
18
+ *
19
+ * Decision #4: getProductName applies NFC + lowercase + whitespace collapse
20
+ * on the raw nvidia-smi product-name output before returning — catalog
21
+ * alias matching depends on byte-level equality after normalization.
22
+ *
23
+ * Decision #8: getProcessUtilization takes a mutable `lastSeenTimestamps`
24
+ * dict and updates it in place. The TS implementation uses
25
+ * `process.hrtime.bigint() → ms` for the per-PID timestamp (nvidia-smi pmon
26
+ * does not expose NVML's sample-buffer timestamp). The accountant uses
27
+ * (endTs - startTs) ms as the per-PID active window proxy.
28
+ */
29
+ import { spawnSync as nodeSpawnSync } from "node:child_process";
30
+ let _spawnFn = (cmd, args, options) => nodeSpawnSync(cmd, args, options);
31
+ /** Test-only — override the spawnSync implementation. Pass `null` to restore. */
32
+ export function _setSpawnFnForTests(fn) {
33
+ _spawnFn =
34
+ fn ??
35
+ ((cmd, args, options) => nodeSpawnSync(cmd, args, options));
36
+ }
37
+ // ─── Warn-once tracking (single-threaded JS → no lock) ──────────────────────
38
+ const _warnedModes = new Set();
39
+ export function _resetWarningStateForTests() {
40
+ _warnedModes.clear();
41
+ }
42
+ function _warnOnce(mode, message) {
43
+ if (_warnedModes.has(mode))
44
+ return;
45
+ _warnedModes.add(mode);
46
+ // eslint-disable-next-line no-console
47
+ console.warn(message);
48
+ }
49
+ // ─── Browser-safety guard ───────────────────────────────────────────────────
50
+ function _isNode() {
51
+ return typeof process !== "undefined" && !!process.versions?.node;
52
+ }
53
+ // ─── Test-only nvidia-smi path override ─────────────────────────────────────
54
+ let _nvidiaSmiPath = "nvidia-smi";
55
+ /** Test-only — override the nvidia-smi binary path. Pass `null` to restore. */
56
+ export function _setNvidiaSmiPathForTests(p) {
57
+ _nvidiaSmiPath = p ?? "nvidia-smi";
58
+ }
59
+ // ─── Helpers ────────────────────────────────────────────────────────────────
60
+ function _runSmi(args) {
61
+ if (!_isNode())
62
+ return { stdout: "", ok: false };
63
+ try {
64
+ const r = _spawnFn(_nvidiaSmiPath, args, {
65
+ encoding: "utf-8",
66
+ timeout: 5000,
67
+ });
68
+ if (r.error || r.status !== 0) {
69
+ return { stdout: "", ok: false };
70
+ }
71
+ return { stdout: r.stdout ?? "", ok: true };
72
+ }
73
+ catch {
74
+ return { stdout: "", ok: false };
75
+ }
76
+ }
77
+ function _normalizeProductName(raw) {
78
+ // NFC → lowercase → whitespace collapse (incl. NBSP / NNBSP / zero-width).
79
+ const nfc = raw.normalize("NFC");
80
+ // String.split(/\s+/) treats Unicode whitespace incl. U+00A0 NBSP as a delimiter.
81
+ return nfc.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
82
+ }
83
+ // ─── Availability + init ────────────────────────────────────────────────────
84
+ /**
85
+ * True when `nvidia-smi` is invokable and exits 0 on the count query.
86
+ *
87
+ * The TS-SDK definition of "available" is "binary works"; in Python this
88
+ * was "pynvml importable AND nvmlInit succeeds". Same end behaviour:
89
+ * downstream code checks this predicate before calling readers.
90
+ */
91
+ export function nvmlAvailable() {
92
+ if (!_isNode())
93
+ return false;
94
+ const r = _runSmi(["--query-gpu=count", "--format=csv,noheader"]);
95
+ if (!r.ok) {
96
+ _warnOnce("gpu_nvidia_smi_not_found", "nvidia-smi not on PATH or exited non-zero; GPU capture disabled. " +
97
+ "Install NVIDIA driver + container toolkit for GPU cost attribution.");
98
+ return false;
99
+ }
100
+ return true;
101
+ }
102
+ /** Call `nvidia-smi --query-gpu=count` once. True on success, false on any failure. */
103
+ export function initNvml() {
104
+ return nvmlAvailable();
105
+ }
106
+ /** No-op on TS; the CLI has no persistent handle. Provided for API parity. */
107
+ export function shutdownNvml() {
108
+ /* no-op */
109
+ }
110
+ // ─── Device enumeration ─────────────────────────────────────────────────────
111
+ /** Number of NVIDIA devices visible to nvidia-smi. `null` on failure. */
112
+ export function getDeviceCount() {
113
+ if (!_isNode())
114
+ return null;
115
+ const r = _runSmi(["--query-gpu=count", "--format=csv,noheader"]);
116
+ if (!r.ok)
117
+ return null;
118
+ // nvidia-smi --query-gpu=count emits one line per GPU, all with the same count value.
119
+ // Take the first non-empty line.
120
+ const first = r.stdout.split("\n").map((s) => s.trim()).find((s) => s);
121
+ if (!first)
122
+ return null;
123
+ const n = parseInt(first, 10);
124
+ if (Number.isNaN(n)) {
125
+ _warnOnce("gpu_device_count_parse", `nvidia-smi count unparseable: ${first}`);
126
+ return null;
127
+ }
128
+ return n;
129
+ }
130
+ // ─── Decision #4: NFC-normalized productName ────────────────────────────────
131
+ /**
132
+ * Return the nvidia-smi product-name for `index`, NFC-normalized + lowercased.
133
+ *
134
+ * Decision #4 — alias matching against the catalog depends on byte-level
135
+ * equality post-normalization.
136
+ */
137
+ export function getProductName(index) {
138
+ if (!_isNode())
139
+ return null;
140
+ const r = _runSmi([
141
+ `--query-gpu=name`,
142
+ `--format=csv,noheader`,
143
+ `-i`,
144
+ String(index),
145
+ ]);
146
+ if (!r.ok) {
147
+ _warnOnce("gpu_product_name_failed", `nvidia-smi --query-gpu=name failed for device ${index}`);
148
+ return null;
149
+ }
150
+ const line = r.stdout.split("\n").map((s) => s.trim()).find((s) => s);
151
+ if (!line)
152
+ return null;
153
+ return _normalizeProductName(line);
154
+ }
155
+ // ─── Per-PID compute processes (Decision #1 measurement-side primitive) ────
156
+ /**
157
+ * List of PIDs currently holding GPU `index` + their VRAM usage in bytes.
158
+ *
159
+ * Returns `null` on permission denied / failure — the Decision #1
160
+ * load-bearing case for non-root containers. The accountant's cgroup walk
161
+ * then degrades to self-PID-only.
162
+ */
163
+ export function getComputeRunningProcesses(index) {
164
+ if (!_isNode())
165
+ return null;
166
+ const r = _runSmi([
167
+ `--query-compute-apps=pid,used_memory`,
168
+ `--format=csv,noheader,nounits`,
169
+ `-i`,
170
+ String(index),
171
+ ]);
172
+ if (!r.ok) {
173
+ _warnOnce("gpu_nvml_permission_denied", `nvidia-smi --query-compute-apps failed for device ${index}; ` +
174
+ `GpuAccountant will degrade to self-PID-only`);
175
+ return null;
176
+ }
177
+ const out = [];
178
+ for (const line of r.stdout.split("\n")) {
179
+ const s = line.trim();
180
+ if (!s)
181
+ continue;
182
+ const parts = s.split(",").map((p) => p.trim());
183
+ if (parts.length < 2)
184
+ continue;
185
+ const pid = parseInt(parts[0], 10);
186
+ if (Number.isNaN(pid))
187
+ continue;
188
+ let memMib = parseInt(parts[1], 10);
189
+ if (Number.isNaN(memMib))
190
+ memMib = 0;
191
+ // nounits ⇒ used_memory is MiB.
192
+ out.push({ pid, usedGpuMemory: memMib * 1024 * 1024 });
193
+ }
194
+ return out;
195
+ }
196
+ // ─── Per-PID utilization (Decision #8 persistent timestamps) ────────────────
197
+ /**
198
+ * Per-PID utilization sampled from `nvidia-smi pmon -c 1 -s u`.
199
+ *
200
+ * Decision #8 — TS uses `Date.now()` as the per-PID timestamp proxy
201
+ * because nvidia-smi pmon does not expose the NVML sample-buffer epoch.
202
+ * The accountant uses (endTs - startTs) ms as the active-GPU-time
203
+ * approximation between snapshots.
204
+ *
205
+ * Updates `lastSeenTimestamps` in place — callers persist across snapshots.
206
+ */
207
+ /**
208
+ * Returns multi-sample-per-PID utilization data.
209
+ *
210
+ * Sprint 2 Theme C / §3.1.1 (B2 TS port). Pre-fix the return type
211
+ * was `Record<number, UtilSample>` — single-latest-sample-per-PID,
212
+ * collapsing the integration data the accountant needs. Now returns
213
+ * `Record<number, UtilSample[]>` with samples sorted by `timeStamp`
214
+ * ascending. nvidia-smi pmon returns one snapshot per call, so the
215
+ * array length is 1 in practice; the type accommodates future
216
+ * sampling implementations that batch multiple observations.
217
+ */
218
+ export function getProcessUtilization(index, lastSeenTimestamps) {
219
+ if (!_isNode())
220
+ return null;
221
+ const r = _runSmi([
222
+ `pmon`,
223
+ `-c`,
224
+ `1`,
225
+ `-s`,
226
+ `u`,
227
+ `-o`,
228
+ `T`,
229
+ `-i`,
230
+ String(index),
231
+ ]);
232
+ if (!r.ok) {
233
+ _warnOnce("gpu_process_utilization_failed", `nvidia-smi pmon failed for device ${index}`);
234
+ return null;
235
+ }
236
+ const out = {};
237
+ const nowMs = Date.now();
238
+ for (const rawLine of r.stdout.split("\n")) {
239
+ const line = rawLine.trim();
240
+ if (!line)
241
+ continue;
242
+ // Skip header lines and N/A rows.
243
+ if (line.startsWith("#"))
244
+ continue;
245
+ // Whitespace-separated columns: gpu pid type sm mem enc dec command
246
+ const parts = line.split(/\s+/);
247
+ if (parts.length < 5)
248
+ continue;
249
+ const pid = parseInt(parts[1], 10);
250
+ if (Number.isNaN(pid))
251
+ continue;
252
+ const sm = parseInt(parts[3], 10);
253
+ const mem = parseInt(parts[4], 10);
254
+ if (Number.isNaN(sm) || Number.isNaN(mem))
255
+ continue;
256
+ const sample = { pid, smUtil: sm, memUtil: mem, timeStamp: nowMs };
257
+ if (out[pid]) {
258
+ out[pid].push(sample);
259
+ }
260
+ else {
261
+ out[pid] = [sample];
262
+ }
263
+ if (nowMs > (lastSeenTimestamps[pid] ?? 0)) {
264
+ lastSeenTimestamps[pid] = nowMs;
265
+ }
266
+ }
267
+ // Sort each PID's samples by timestamp ascending — the integrator
268
+ // assumes monotonic ordering.
269
+ for (const pid in out) {
270
+ out[pid].sort((a, b) => a.timeStamp - b.timeStamp);
271
+ }
272
+ return out;
273
+ }
274
+ // ─── Memory + MIG ───────────────────────────────────────────────────────────
275
+ /** Device-level used / total VRAM (bytes). `null` on failure. */
276
+ export function getMemoryInfo(index) {
277
+ if (!_isNode())
278
+ return null;
279
+ const r = _runSmi([
280
+ `--query-gpu=memory.used,memory.total`,
281
+ `--format=csv,noheader,nounits`,
282
+ `-i`,
283
+ String(index),
284
+ ]);
285
+ if (!r.ok) {
286
+ _warnOnce("gpu_memory_info_failed", `nvidia-smi --query-gpu=memory.used,memory.total failed for device ${index}`);
287
+ return null;
288
+ }
289
+ const line = r.stdout.split("\n").map((s) => s.trim()).find((s) => s);
290
+ if (!line)
291
+ return null;
292
+ const parts = line.split(",").map((s) => s.trim());
293
+ if (parts.length < 2)
294
+ return null;
295
+ const usedMib = parseInt(parts[0], 10);
296
+ const totalMib = parseInt(parts[1], 10);
297
+ if (Number.isNaN(usedMib) || Number.isNaN(totalMib))
298
+ return null;
299
+ return {
300
+ usedBytes: usedMib * 1024 * 1024,
301
+ totalBytes: totalMib * 1024 * 1024,
302
+ };
303
+ }
304
+ /** True when MIG is enabled on this device (Decision #2 detection). */
305
+ export function getMigMode(index) {
306
+ if (!_isNode())
307
+ return false;
308
+ const r = _runSmi([
309
+ `--query-gpu=mig.mode.current`,
310
+ `--format=csv,noheader`,
311
+ `-i`,
312
+ String(index),
313
+ ]);
314
+ if (!r.ok) {
315
+ // Older GPUs without MIG support → fail-silent (NOT an error).
316
+ return false;
317
+ }
318
+ const line = r.stdout.split("\n").map((s) => s.trim()).find((s) => s);
319
+ if (!line)
320
+ return false;
321
+ return line.toLowerCase() === "enabled";
322
+ }
323
+ //# sourceMappingURL=nvml-reader.js.map