agent-orchestrator-kit 0.14.1 → 0.15.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/CHANGELOG.md +6 -0
- package/README.md +8 -3
- package/bin/agent-orchestrator.js +28 -2
- package/package.json +1 -1
- package/templates/scripts/cursor-spend-collect.cjs +24 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,13 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.15.0] - 2026-09-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **`costUsdTotal` — one USD figure per change, phase, platform, model, and session.** Each session contributes its billed `costUsd` when present, otherwise its `costUsdEstimated`; sums are rounded to 4 decimals. A change that ran on Amp (billed `$14.48`) plus Claude (`~$5.16`) and Cursor (`~$1.43`) now carries `spend.costUsdTotal: 21.0779` instead of forcing a dashboard to pick `costUsd` and drop the estimated platforms. `costUsd` and `costUsdEstimated` stay separate fields and Amp credits stay out of every USD field. `metrics --summary-json` carries the field; the human `cost:` line prints `$21.08 ($14.48 billed + ~$6.60 est.)`.
|
|
11
|
+
|
|
7
12
|
### Fixed
|
|
13
|
+
- **Billed sums are rounded like estimates.** `spendByModel[].costUsd`, `spendByPlatform.*.costUsd`, `phases.*.costUsd`, and `spend.costUsd` stored raw float sums such as `4.4399999999999995` and `0.009000000000000001`; they are now `Math.round(x * 10000) / 10000`, the same rule `costUsdEstimated` already had.
|
|
8
14
|
- **Cache-split cost estimates survive the session recompute.** `applyCollectedSessionFields` recomputed `byModel[].costUsdEstimated` from `inputTokens` + `outputTokens` alone and overwrote the value the adapters had already produced. Since `inputTokens` includes `cache_read_*` / `cache_creation_*`, every cached token was billed at the full input rate: a real Claude change with 3.5M tokens estimated `$30.11` instead of `$5.16`, and Cursor rows lost their own rate table to the Claude `$3/$15` fallback. The recompute now only fills rows that have no adapter estimate, so ESTIMATE-ALL still covers Amp rows without `Cost:` while `claude-opus-5` with 100k input + 900k cache read + 10k output stays at `$1.20`.
|
|
9
15
|
|
|
10
16
|
## [0.14.1] - 2026-09-08
|
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ npx agent-orchestrator-kit@latest init --profile generic --ci gitlab --spec-veri
|
|
|
74
74
|
|
|
75
75
|
See [Installation](#installation) for profile/CI options.
|
|
76
76
|
|
|
77
|
-
**🔄 Already have the kit installed? Upgrade to latest (v0.
|
|
77
|
+
**🔄 Already have the kit installed? Upgrade to latest (v0.15.0 adds `costUsdTotal`, one USD figure per change / phase / platform / model that sums billed Amp with estimated Cursor and Claude, rounds billed sums like estimates, and keeps cache-split Claude estimates from being re-billed at the full input rate):**
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
80
|
npx agent-orchestrator-kit@latest update
|
|
@@ -816,7 +816,7 @@ Schema v2 stores compact `sourceIds`, `sourceTotals`, and `byModel` per session
|
|
|
816
816
|
- **Locked client** — `--restore` records `pending.platform` and Amp `pending.threadId` before phase work. Persist follows that client’s flow even if persist runs in another shell (no `AMP_*` / `CURSOR_*`). Amp: `amp threads export` plus `amp threads usage --details` (`AOK_AMP_BIN`) and local `threads/*.json`. Export supplies `model` / tokens / `agentMode`; usage supplies billed `$`. If Amp runs tools over a pipe (`/dev/null`), thread id comes from `amp threads list`, not stale `session.json` `lastThreadId`. When env and Amp parent do not win, restore locks a fresh `session.json` `lastThreadId` as `amp-session-last`. Cursor: spend hook file. Claude: `~/.claude/projects`. `--collect` still runs all three adapters.
|
|
817
817
|
- **Cursor spend hook (optional)** — Cursor never writes token usage to disk, so the kit can install `scripts/cursor-spend-hook.cjs` plus `.cursor/hooks.json` entries (`stop` / `subagentStop` / `afterAgentResponse`) in `init` / `update` / `sync` / `mcp-setup`: the hook appends each turn's tokens to gitignored `.agents/spend/cursor-usage.jsonl`. After a successful `stop` / `afterAgentResponse` append the hook runs leftover (fail-open, no stdout). Hook and collect resolve the consumer in a multi-root window (not the first cwd with `.agents` or `openspec`). Persist auto-reads that file when the locked client is Cursor. Persist and restore do not self-heal the hook. `sessionEnd` still runs `scripts/cursor-spend-collect.cjs`. Restart Cursor once after the first install. `status` shows a `Spend capture` section. Claude JSONL remains a fallback. Amp web/CLI spend is taken from `amp threads export` (tokens, model, `agentMode`) and `amp threads usage` (billed USD).
|
|
818
818
|
|
|
819
|
-
Aggregates are recomputed on every write: per-phase totals (`startedAt`, `endedAt`, `leadTimeMs` from that phase’s sessions, `durationMs` = sum of session work time — not `totals.leadTimeMs` and not `endedAt − startedAt`, tokens, `costUsd`, `costUsdEstimated` to 4 decimals, `sessions`, `roles`, `models`) plus overall `totals` (`sessions`, `cloudSessions`, `durationMs` = sum of session work time, `leadTimeMs` = wall clock from first session start to last session end), `spend` (USD only), and separate **by platform** / **by model** tables. Numbers are null-honest: a metric nobody reported stays `null`, never a fake `0`. No single total that adds Amp credits to USD.
|
|
819
|
+
Aggregates are recomputed on every write: per-phase totals (`startedAt`, `endedAt`, `leadTimeMs` from that phase’s sessions, `durationMs` = sum of session work time — not `totals.leadTimeMs` and not `endedAt − startedAt`, tokens, `costUsd`, `costUsdEstimated`, `costUsdTotal` to 4 decimals, `sessions`, `roles`, `models`) plus overall `totals` (`sessions`, `cloudSessions`, `durationMs` = sum of session work time, `leadTimeMs` = wall clock from first session start to last session end), `spend` (USD only), and separate **by platform** / **by model** tables. Numbers are null-honest: a metric nobody reported stays `null`, never a fake `0`. No single total that adds Amp credits to USD. `costUsdTotal` is the one USD figure per change, phase, platform, model, and session: each session contributes its billed `costUsd` when present, otherwise its `costUsdEstimated`, so a change that ran on Amp (billed) plus Cursor and Claude (estimated) sums all three platforms instead of showing only the billed part. Amp credits never enter it. The human `cost:` line prints it as `$21.08 ($14.48 billed + ~$6.60 est.)`.
|
|
820
820
|
|
|
821
821
|
Fill `## Metrics` in `handoff.md` **before** persist (unknown is fine; do not invent `0`):
|
|
822
822
|
|
|
@@ -843,7 +843,7 @@ npx agent-orchestrator-kit archive add-thing --sync # finalize + the same t
|
|
|
843
843
|
|
|
844
844
|
#### Для дашбордів
|
|
845
845
|
|
|
846
|
-
Use only `phases.<phase>.startedAt`, `endedAt`, and `durationMs` for phase boundaries and duration. Use `totals.leadTimeMs` only for the whole change. Git log MUST NOT be used for phase boundaries; the kit does not provide per-phase commit counts. `metrics <name> --summary-json` returns only aggregate `totals`, `phases`, and spend maps, without sessions or commits.
|
|
846
|
+
Use only `phases.<phase>.startedAt`, `endedAt`, and `durationMs` for phase boundaries and duration. Use `totals.leadTimeMs` only for the whole change. Git log MUST NOT be used for phase boundaries; the kit does not provide per-phase commit counts. `metrics <name> --summary-json` returns only aggregate `totals`, `phases`, and spend maps, without sessions or commits. For the headline cost use `spend.costUsdTotal` (and `phases.<phase>.costUsdTotal`, `spendByPlatform.<platform>.costUsdTotal`, `spendByModel[].costUsdTotal`): `costUsd` alone is only the billed part and `costUsdEstimated` alone is only the estimated part, so picking one of them drops every platform that reported the other.
|
|
847
847
|
|
|
848
848
|
Recording is on by default and never a persist/archive/`gate-check` gate; opt out per persist with `--no-metrics`. Persist and archive collect the locked client without `--collect`; `--collect` runs every adapter. Flags (`--model`, `--platform`, `--input-tokens`, …) override session totals in `metrics.json` and do not rewrite the `## Metrics` section.
|
|
849
849
|
|
|
@@ -1017,6 +1017,11 @@ Phase bounds and non-goals: [`openspec/specs/agentic-factory-roadmap/spec.md`](o
|
|
|
1017
1017
|
|
|
1018
1018
|
## Changelog
|
|
1019
1019
|
|
|
1020
|
+
### 0.15.0
|
|
1021
|
+
- **`costUsdTotal`** on `spend`, `phases.*`, `spendByPlatform.*`, `spendByModel[]`, and sessions — billed `costUsd` when present, otherwise `costUsdEstimated`, per session; a change that ran on Amp (billed) plus Cursor and Claude (estimated) now sums all three platforms instead of showing only the billed part; the human `cost:` line prints `$21.08 ($14.48 billed + ~$6.60 est.)` and `--summary-json` carries the field
|
|
1022
|
+
- Billed `costUsd` aggregates are rounded to 4 decimals like estimates (no more `4.4399999999999995` in `spendByModel`)
|
|
1023
|
+
- Cache-split Claude cost estimates survive the session recompute instead of being re-billed at the full input rate (`$30.11` → `$5.16` on a 3.5M-token change)
|
|
1024
|
+
|
|
1020
1025
|
### 0.14.1
|
|
1021
1026
|
- **Memory graph survives `handoff` persist** — JSONL graphs were misread as an aggregate document and returned empty, so each persist overwrote the file with only the current change; all `Decision:*`, other changes, and relations were lost
|
|
1022
1027
|
- `/opsx:*` commands now sync to `.cursor/commands/` (flat) and `.claude/commands/opsx/` (namespaced), so the documented slash commands exist in both IDEs
|
|
@@ -1710,12 +1710,18 @@ function roundUsd4(x) {
|
|
|
1710
1710
|
return Math.round(Number(x) * 10000) / 10000;
|
|
1711
1711
|
}
|
|
1712
1712
|
|
|
1713
|
+
function costUsdTotalOf(obj) {
|
|
1714
|
+
const billed = numOrNull(obj && obj.costUsd);
|
|
1715
|
+
if (billed != null) return billed;
|
|
1716
|
+
return numOrNull(obj && obj.costUsdEstimated);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1713
1719
|
function metricsFilePath(projectDir, changeName) {
|
|
1714
1720
|
return join(projectDir, 'openspec', 'changes', changeName, 'metrics.json');
|
|
1715
1721
|
}
|
|
1716
1722
|
|
|
1717
1723
|
function emptySpendTotals() {
|
|
1718
|
-
return { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null };
|
|
1724
|
+
return { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null, costUsdTotal: null };
|
|
1719
1725
|
}
|
|
1720
1726
|
|
|
1721
1727
|
function emptyPlatformSpend(source = 'none') {
|
|
@@ -1726,6 +1732,7 @@ function emptyPlatformSpend(source = 'none') {
|
|
|
1726
1732
|
costUsd: null,
|
|
1727
1733
|
ampCredits: null,
|
|
1728
1734
|
costUsdEstimated: null,
|
|
1735
|
+
costUsdTotal: null,
|
|
1729
1736
|
source,
|
|
1730
1737
|
};
|
|
1731
1738
|
}
|
|
@@ -2549,6 +2556,7 @@ function spendTuple(obj) {
|
|
|
2549
2556
|
costUsd: numOrNull(obj && obj.costUsd),
|
|
2550
2557
|
ampCredits: numOrNull(obj && obj.ampCredits),
|
|
2551
2558
|
costUsdEstimated: numOrNull(obj && obj.costUsdEstimated),
|
|
2559
|
+
costUsdTotal: numOrNull(obj && obj.costUsdTotal) ?? costUsdTotalOf(obj),
|
|
2552
2560
|
};
|
|
2553
2561
|
}
|
|
2554
2562
|
|
|
@@ -2568,6 +2576,7 @@ function addSpendNums(target, nums) {
|
|
|
2568
2576
|
target.costUsd = addNullable(target.costUsd, nums.costUsd);
|
|
2569
2577
|
target.ampCredits = addNullable(target.ampCredits, nums.ampCredits);
|
|
2570
2578
|
target.costUsdEstimated = addNullable(target.costUsdEstimated, nums.costUsdEstimated);
|
|
2579
|
+
target.costUsdTotal = addNullable(target.costUsdTotal, nums.costUsdTotal);
|
|
2571
2580
|
}
|
|
2572
2581
|
|
|
2573
2582
|
function recomputeSpendMaps(metrics) {
|
|
@@ -2586,6 +2595,7 @@ function recomputeSpendMaps(metrics) {
|
|
|
2586
2595
|
costUsd: null,
|
|
2587
2596
|
ampCredits: null,
|
|
2588
2597
|
costUsdEstimated: null,
|
|
2598
|
+
costUsdTotal: null,
|
|
2589
2599
|
};
|
|
2590
2600
|
addSpendNums(row, nums);
|
|
2591
2601
|
byModel.set(key, row);
|
|
@@ -2606,10 +2616,14 @@ function recomputeSpendMaps(metrics) {
|
|
|
2606
2616
|
}
|
|
2607
2617
|
}
|
|
2608
2618
|
for (const key of Object.keys(byPlatform)) {
|
|
2619
|
+
byPlatform[key].costUsd = roundUsd4(byPlatform[key].costUsd);
|
|
2609
2620
|
byPlatform[key].costUsdEstimated = roundUsd4(byPlatform[key].costUsdEstimated);
|
|
2621
|
+
byPlatform[key].costUsdTotal = roundUsd4(byPlatform[key].costUsdTotal);
|
|
2610
2622
|
}
|
|
2611
2623
|
for (const row of byModel.values()) {
|
|
2624
|
+
row.costUsd = roundUsd4(row.costUsd);
|
|
2612
2625
|
row.costUsdEstimated = roundUsd4(row.costUsdEstimated);
|
|
2626
|
+
row.costUsdTotal = roundUsd4(row.costUsdTotal);
|
|
2613
2627
|
}
|
|
2614
2628
|
metrics.spendByPlatform = byPlatform;
|
|
2615
2629
|
metrics.spendByModel = [...byModel.values()];
|
|
@@ -2661,6 +2675,13 @@ function recomputeMetricsAggregates(metrics) {
|
|
|
2661
2675
|
phase[spendKey] = addNullable(phase[spendKey], value);
|
|
2662
2676
|
spend[spendKey] = addNullable(spend[spendKey], value);
|
|
2663
2677
|
}
|
|
2678
|
+
const sessionCostUsdTotal = costUsdTotalOf({
|
|
2679
|
+
costUsd: sessionFieldOrSources(session, 'costUsd'),
|
|
2680
|
+
costUsdEstimated: sessionFieldOrSources(session, 'costUsdEstimated'),
|
|
2681
|
+
});
|
|
2682
|
+
session.costUsdTotal = roundUsd4(sessionCostUsdTotal);
|
|
2683
|
+
phase.costUsdTotal = addNullable(phase.costUsdTotal, sessionCostUsdTotal);
|
|
2684
|
+
spend.costUsdTotal = addNullable(spend.costUsdTotal, sessionCostUsdTotal);
|
|
2664
2685
|
if (session.role && !phase.agents.includes(session.role)) phase.agents.push(session.role);
|
|
2665
2686
|
if (session.model && !phase.models.includes(session.model)) phase.models.push(session.model);
|
|
2666
2687
|
if (Array.isArray(session.models)) {
|
|
@@ -2679,9 +2700,13 @@ function recomputeMetricsAggregates(metrics) {
|
|
|
2679
2700
|
phase.leadTimeMs = Number.isFinite(startMs) && Number.isFinite(endMs)
|
|
2680
2701
|
? Math.max(0, endMs - startMs)
|
|
2681
2702
|
: null;
|
|
2703
|
+
phase.costUsd = roundUsd4(phase.costUsd);
|
|
2682
2704
|
phase.costUsdEstimated = roundUsd4(phase.costUsdEstimated);
|
|
2705
|
+
phase.costUsdTotal = roundUsd4(phase.costUsdTotal);
|
|
2683
2706
|
}
|
|
2707
|
+
spend.costUsd = roundUsd4(spend.costUsd);
|
|
2684
2708
|
spend.costUsdEstimated = roundUsd4(spend.costUsdEstimated);
|
|
2709
|
+
spend.costUsdTotal = roundUsd4(spend.costUsdTotal);
|
|
2685
2710
|
metrics.phases = phases;
|
|
2686
2711
|
metrics.totals = totals;
|
|
2687
2712
|
metrics.spend = spend;
|
|
@@ -2908,7 +2933,8 @@ function formatMetricsCostLine(spend) {
|
|
|
2908
2933
|
const estimated = spend && spend.costUsdEstimated;
|
|
2909
2934
|
if (billed == null && estimated == null) return '—';
|
|
2910
2935
|
if (billed != null && estimated != null) {
|
|
2911
|
-
|
|
2936
|
+
const total = spend.costUsdTotal != null ? spend.costUsdTotal : billed + estimated;
|
|
2937
|
+
return `${formatMetricsCost(total)} (${formatMetricsCost(billed)} billed + ~${formatMetricsCost(estimated)} est.)`;
|
|
2912
2938
|
}
|
|
2913
2939
|
if (billed != null) return formatMetricsCost(billed);
|
|
2914
2940
|
return `~${formatMetricsCost(estimated)} est.`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-orchestrator-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Universal AI agent orchestration kit for Cursor, Claude Code, and Amp Code — spec-driven OpenSpec pipeline, conductor subagents, durable session handoff, factory gates and MCP setup, cloud-agent handoff, and optional local Figma PAT setup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agent",
|
|
@@ -23,6 +23,12 @@ function roundUsd4(x) {
|
|
|
23
23
|
return Math.round(Number(x) * 10000) / 10000;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function costUsdTotalOf(obj) {
|
|
27
|
+
const billed = numOrNull(obj && obj.costUsd);
|
|
28
|
+
if (billed != null) return billed;
|
|
29
|
+
return numOrNull(obj && obj.costUsdEstimated);
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
function timestampMs(value) {
|
|
27
33
|
if (value == null || value === '') return NaN;
|
|
28
34
|
const ms = Date.parse(value);
|
|
@@ -435,6 +441,7 @@ function emptyPlatform(source = 'none') {
|
|
|
435
441
|
costUsd: null,
|
|
436
442
|
ampCredits: null,
|
|
437
443
|
costUsdEstimated: null,
|
|
444
|
+
costUsdTotal: null,
|
|
438
445
|
source,
|
|
439
446
|
};
|
|
440
447
|
}
|
|
@@ -442,7 +449,7 @@ function emptyPlatform(source = 'none') {
|
|
|
442
449
|
function recompute(metrics) {
|
|
443
450
|
const phases = {};
|
|
444
451
|
const totals = { sessions: 0, durationMs: null, leadTimeMs: null, cloudSessions: 0 };
|
|
445
|
-
const spend = { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null };
|
|
452
|
+
const spend = { inputTokens: null, outputTokens: null, totalTokens: null, costUsd: null, costUsdEstimated: null, costUsdTotal: null };
|
|
446
453
|
const byPlatform = {
|
|
447
454
|
cursor: emptyPlatform(),
|
|
448
455
|
claude: emptyPlatform(),
|
|
@@ -477,6 +484,7 @@ function recompute(metrics) {
|
|
|
477
484
|
totalTokens: null,
|
|
478
485
|
costUsd: null,
|
|
479
486
|
costUsdEstimated: null,
|
|
487
|
+
costUsdTotal: null,
|
|
480
488
|
agents: [],
|
|
481
489
|
models: [],
|
|
482
490
|
};
|
|
@@ -493,6 +501,10 @@ function recompute(metrics) {
|
|
|
493
501
|
phase[spendKey] = addNullable(phase[spendKey], value);
|
|
494
502
|
spend[spendKey] = addNullable(spend[spendKey], value);
|
|
495
503
|
}
|
|
504
|
+
const sessionCostUsdTotal = costUsdTotalOf(session);
|
|
505
|
+
session.costUsdTotal = roundUsd4(sessionCostUsdTotal);
|
|
506
|
+
phase.costUsdTotal = addNullable(phase.costUsdTotal, sessionCostUsdTotal);
|
|
507
|
+
spend.costUsdTotal = addNullable(spend.costUsdTotal, sessionCostUsdTotal);
|
|
496
508
|
if (session.role && !phase.agents.includes(session.role)) phase.agents.push(session.role);
|
|
497
509
|
if (session.model && !phase.models.includes(session.model)) phase.models.push(session.model);
|
|
498
510
|
if (Array.isArray(session.models)) {
|
|
@@ -508,6 +520,7 @@ function recompute(metrics) {
|
|
|
508
520
|
for (const key of ['inputTokens', 'outputTokens', 'totalTokens', 'costUsd', 'ampCredits', 'costUsdEstimated']) {
|
|
509
521
|
bucket[key] = addNullable(bucket[key], numOrNull(session[key]));
|
|
510
522
|
}
|
|
523
|
+
bucket.costUsdTotal = addNullable(bucket.costUsdTotal, sessionCostUsdTotal);
|
|
511
524
|
if ((session.sourceIds || []).length) bucket.source = platform === 'cursor' ? 'cursor-hook' : `${platform}-jsonl`;
|
|
512
525
|
}
|
|
513
526
|
const modelRows = (session.byModel || []).length ? session.byModel : [session];
|
|
@@ -523,6 +536,7 @@ function recompute(metrics) {
|
|
|
523
536
|
costUsd: null,
|
|
524
537
|
ampCredits: null,
|
|
525
538
|
costUsdEstimated: null,
|
|
539
|
+
costUsdTotal: null,
|
|
526
540
|
};
|
|
527
541
|
row.inputTokens = addNullable(row.inputTokens, numOrNull(src.inputTokens));
|
|
528
542
|
row.outputTokens = addNullable(row.outputTokens, numOrNull(src.outputTokens));
|
|
@@ -530,6 +544,7 @@ function recompute(metrics) {
|
|
|
530
544
|
row.costUsd = addNullable(row.costUsd, numOrNull(src.costUsd));
|
|
531
545
|
row.ampCredits = addNullable(row.ampCredits, numOrNull(src.ampCredits));
|
|
532
546
|
row.costUsdEstimated = addNullable(row.costUsdEstimated, numOrNull(src.costUsdEstimated));
|
|
547
|
+
row.costUsdTotal = addNullable(row.costUsdTotal, costUsdTotalOf(src));
|
|
533
548
|
byModel.set(modelKey, row);
|
|
534
549
|
}
|
|
535
550
|
}
|
|
@@ -544,14 +559,22 @@ function recompute(metrics) {
|
|
|
544
559
|
phase.leadTimeMs = Number.isFinite(startMs) && Number.isFinite(endMs)
|
|
545
560
|
? Math.max(0, endMs - startMs)
|
|
546
561
|
: null;
|
|
562
|
+
phase.costUsd = roundUsd4(phase.costUsd);
|
|
547
563
|
phase.costUsdEstimated = roundUsd4(phase.costUsdEstimated);
|
|
564
|
+
phase.costUsdTotal = roundUsd4(phase.costUsdTotal);
|
|
548
565
|
}
|
|
566
|
+
spend.costUsd = roundUsd4(spend.costUsd);
|
|
549
567
|
spend.costUsdEstimated = roundUsd4(spend.costUsdEstimated);
|
|
568
|
+
spend.costUsdTotal = roundUsd4(spend.costUsdTotal);
|
|
550
569
|
for (const bucket of Object.values(byPlatform)) {
|
|
570
|
+
bucket.costUsd = roundUsd4(bucket.costUsd);
|
|
551
571
|
bucket.costUsdEstimated = roundUsd4(bucket.costUsdEstimated);
|
|
572
|
+
bucket.costUsdTotal = roundUsd4(bucket.costUsdTotal);
|
|
552
573
|
}
|
|
553
574
|
for (const row of byModel.values()) {
|
|
575
|
+
row.costUsd = roundUsd4(row.costUsd);
|
|
554
576
|
row.costUsdEstimated = roundUsd4(row.costUsdEstimated);
|
|
577
|
+
row.costUsdTotal = roundUsd4(row.costUsdTotal);
|
|
555
578
|
}
|
|
556
579
|
metrics.phases = phases;
|
|
557
580
|
metrics.totals = totals;
|