@jadenrazo/cloudcost-mcp 1.2.1 → 1.2.2
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 +143 -0
- package/README.md +24 -12
- package/data/aws-pricing/metadata.json +22 -2
- package/data/azure-pricing/metadata.json +297 -2
- package/data/gcp-pricing/cloud-storage.json +222 -16
- package/data/gcp-pricing/compute-engine.json +2051 -253
- package/data/gcp-pricing/metadata.json +19 -5
- package/data/gcp-pricing/persistent-disk.json +222 -16
- package/dist/{chunk-CGMU4ETN.js → chunk-4P3I7BSL.js} +73 -664
- package/dist/{chunk-LWTVE7UY.js → chunk-R2BI5AVN.js} +13 -7
- package/dist/cli.js +2 -2
- package/dist/index.js +2 -2
- package/dist/{loader-CMBV7346.js → loader-G7WW74B6.js} +2 -2
- package/package.json +12 -10
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
getRegionMappings,
|
|
13
13
|
getRegionPriceMultipliers,
|
|
14
14
|
getStorageMap
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-R2BI5AVN.js";
|
|
16
16
|
|
|
17
17
|
// src/config.ts
|
|
18
18
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -1422,7 +1422,6 @@ var AwsBulkLoader = class {
|
|
|
1422
1422
|
return false;
|
|
1423
1423
|
}
|
|
1424
1424
|
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
1425
|
-
this.cache.beginBatch();
|
|
1426
1425
|
let colInstanceType = -1;
|
|
1427
1426
|
let colOS = -1;
|
|
1428
1427
|
let colTenancy = -1;
|
|
@@ -1433,8 +1432,45 @@ var AwsBulkLoader = class {
|
|
|
1433
1432
|
let colUnit = -1;
|
|
1434
1433
|
let headerFound = false;
|
|
1435
1434
|
let leftover = "";
|
|
1436
|
-
|
|
1435
|
+
const lowestPriceByKey = /* @__PURE__ */ new Map();
|
|
1437
1436
|
let effectiveDate = (/* @__PURE__ */ new Date()).toISOString();
|
|
1437
|
+
const considerPriceLine = (line) => {
|
|
1438
|
+
if (!line.includes("OnDemand") || !line.includes("Compute Instance")) {
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
const fields = parseCsvLine(line);
|
|
1442
|
+
if (colProductFamily !== -1 && fields[colProductFamily] !== "Compute Instance") return;
|
|
1443
|
+
if (colTenancy !== -1 && fields[colTenancy] !== "Shared") return;
|
|
1444
|
+
if (colTermType !== -1 && fields[colTermType] !== "OnDemand") return;
|
|
1445
|
+
if (colCapacityStatus !== -1 && fields[colCapacityStatus] !== "Used") return;
|
|
1446
|
+
const instanceType = fields[colInstanceType] ?? "";
|
|
1447
|
+
const os = fields[colOS] ?? "";
|
|
1448
|
+
const rawPrice = fields[colPrice] ?? "";
|
|
1449
|
+
const unit = colUnit !== -1 ? fields[colUnit] ?? "Hrs" : "Hrs";
|
|
1450
|
+
if (!instanceType || !os || !rawPrice) return;
|
|
1451
|
+
const priceValue = parseFloat(rawPrice);
|
|
1452
|
+
if (!isFinite(priceValue) || priceValue <= 0) return;
|
|
1453
|
+
const csvCacheKey = `aws/ec2/${region}/${instanceType.toLowerCase()}/${os.toLowerCase()}`;
|
|
1454
|
+
const current = lowestPriceByKey.get(csvCacheKey);
|
|
1455
|
+
if (current && current.price_per_unit <= priceValue) return;
|
|
1456
|
+
lowestPriceByKey.set(csvCacheKey, {
|
|
1457
|
+
provider: "aws",
|
|
1458
|
+
service: "ec2",
|
|
1459
|
+
resource_type: instanceType,
|
|
1460
|
+
region,
|
|
1461
|
+
unit,
|
|
1462
|
+
price_per_unit: priceValue,
|
|
1463
|
+
currency: "USD",
|
|
1464
|
+
description: `AWS EC2 ${instanceType} (${os}) on-demand`,
|
|
1465
|
+
attributes: {
|
|
1466
|
+
instance_type: instanceType,
|
|
1467
|
+
operating_system: os,
|
|
1468
|
+
tenancy: "Shared",
|
|
1469
|
+
pricing_source: "live"
|
|
1470
|
+
},
|
|
1471
|
+
effective_date: effectiveDate
|
|
1472
|
+
});
|
|
1473
|
+
};
|
|
1438
1474
|
while (true) {
|
|
1439
1475
|
const { value, done } = await reader.read();
|
|
1440
1476
|
if (done) break;
|
|
@@ -1476,110 +1512,29 @@ var AwsBulkLoader = class {
|
|
|
1476
1512
|
colOS,
|
|
1477
1513
|
colPrice
|
|
1478
1514
|
});
|
|
1479
|
-
this.cache.rollbackBatch();
|
|
1480
1515
|
return false;
|
|
1481
1516
|
}
|
|
1482
1517
|
headerFound = true;
|
|
1483
1518
|
}
|
|
1484
1519
|
continue;
|
|
1485
1520
|
}
|
|
1486
|
-
|
|
1487
|
-
continue;
|
|
1488
|
-
}
|
|
1489
|
-
const fields = parseCsvLine(line);
|
|
1490
|
-
if (colProductFamily !== -1) {
|
|
1491
|
-
const pf = fields[colProductFamily] ?? "";
|
|
1492
|
-
if (pf !== "Compute Instance") continue;
|
|
1493
|
-
}
|
|
1494
|
-
if (colTenancy !== -1) {
|
|
1495
|
-
const tenancy = fields[colTenancy] ?? "";
|
|
1496
|
-
if (tenancy !== "Shared") continue;
|
|
1497
|
-
}
|
|
1498
|
-
if (colTermType !== -1) {
|
|
1499
|
-
const termType = fields[colTermType] ?? "";
|
|
1500
|
-
if (termType !== "OnDemand") continue;
|
|
1501
|
-
}
|
|
1502
|
-
if (colCapacityStatus !== -1) {
|
|
1503
|
-
const cap = fields[colCapacityStatus] ?? "";
|
|
1504
|
-
if (cap !== "Used") continue;
|
|
1505
|
-
}
|
|
1506
|
-
const instanceType = fields[colInstanceType] ?? "";
|
|
1507
|
-
const os = fields[colOS] ?? "";
|
|
1508
|
-
const rawPrice = fields[colPrice] ?? "";
|
|
1509
|
-
const unit = colUnit !== -1 ? fields[colUnit] ?? "Hrs" : "Hrs";
|
|
1510
|
-
if (!instanceType || !os || !rawPrice) continue;
|
|
1511
|
-
const priceValue = parseFloat(rawPrice);
|
|
1512
|
-
if (!isFinite(priceValue) || priceValue <= 0) continue;
|
|
1513
|
-
const csvCacheKey = `aws/ec2/${region}/${instanceType.toLowerCase()}/${os.toLowerCase()}`;
|
|
1514
|
-
const normalized = {
|
|
1515
|
-
provider: "aws",
|
|
1516
|
-
service: "ec2",
|
|
1517
|
-
resource_type: instanceType,
|
|
1518
|
-
region,
|
|
1519
|
-
unit,
|
|
1520
|
-
price_per_unit: priceValue,
|
|
1521
|
-
currency: "USD",
|
|
1522
|
-
description: `AWS EC2 ${instanceType} (${os}) on-demand`,
|
|
1523
|
-
attributes: {
|
|
1524
|
-
instance_type: instanceType,
|
|
1525
|
-
operating_system: os,
|
|
1526
|
-
tenancy: "Shared",
|
|
1527
|
-
pricing_source: "live"
|
|
1528
|
-
},
|
|
1529
|
-
effective_date: effectiveDate
|
|
1530
|
-
};
|
|
1531
|
-
this.cache.set(csvCacheKey, normalized, "aws", "ec2", region, this.ttlSeconds);
|
|
1532
|
-
cachedCount++;
|
|
1521
|
+
considerPriceLine(line);
|
|
1533
1522
|
}
|
|
1534
1523
|
}
|
|
1535
1524
|
if (leftover.trim() && headerFound) {
|
|
1536
|
-
|
|
1537
|
-
if (line.includes("OnDemand") && line.includes("Compute Instance")) {
|
|
1538
|
-
const fields = parseCsvLine(line);
|
|
1539
|
-
const instanceType = fields[colInstanceType] ?? "";
|
|
1540
|
-
const os = fields[colOS] ?? "";
|
|
1541
|
-
const rawPrice = fields[colPrice] ?? "";
|
|
1542
|
-
const unit = colUnit !== -1 ? fields[colUnit] ?? "Hrs" : "Hrs";
|
|
1543
|
-
const priceValue = parseFloat(rawPrice);
|
|
1544
|
-
if (instanceType && os && isFinite(priceValue) && priceValue > 0) {
|
|
1545
|
-
const csvCacheKey = `aws/ec2/${region}/${instanceType.toLowerCase()}/${os.toLowerCase()}`;
|
|
1546
|
-
this.cache.set(
|
|
1547
|
-
csvCacheKey,
|
|
1548
|
-
{
|
|
1549
|
-
provider: "aws",
|
|
1550
|
-
service: "ec2",
|
|
1551
|
-
resource_type: instanceType,
|
|
1552
|
-
region,
|
|
1553
|
-
unit,
|
|
1554
|
-
price_per_unit: priceValue,
|
|
1555
|
-
currency: "USD",
|
|
1556
|
-
description: `AWS EC2 ${instanceType} (${os}) on-demand`,
|
|
1557
|
-
attributes: {
|
|
1558
|
-
instance_type: instanceType,
|
|
1559
|
-
operating_system: os,
|
|
1560
|
-
tenancy: "Shared",
|
|
1561
|
-
pricing_source: "live"
|
|
1562
|
-
},
|
|
1563
|
-
effective_date: effectiveDate
|
|
1564
|
-
},
|
|
1565
|
-
"aws",
|
|
1566
|
-
"ec2",
|
|
1567
|
-
region,
|
|
1568
|
-
this.ttlSeconds
|
|
1569
|
-
);
|
|
1570
|
-
cachedCount++;
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1525
|
+
considerPriceLine(leftover.trimEnd());
|
|
1573
1526
|
}
|
|
1527
|
+
const cachedCount = lowestPriceByKey.size;
|
|
1574
1528
|
if (cachedCount > 0) {
|
|
1529
|
+
this.cache.beginBatch();
|
|
1530
|
+
for (const [csvCacheKey, normalized] of lowestPriceByKey) {
|
|
1531
|
+
this.cache.set(csvCacheKey, normalized, "aws", "ec2", region, this.ttlSeconds);
|
|
1532
|
+
}
|
|
1575
1533
|
this.cache.endBatch();
|
|
1576
|
-
} else {
|
|
1577
|
-
this.cache.rollbackBatch();
|
|
1578
1534
|
}
|
|
1579
1535
|
logger.debug("AWS EC2 CSV streaming complete", { region, cachedCount });
|
|
1580
1536
|
return cachedCount > 0;
|
|
1581
1537
|
} catch (err) {
|
|
1582
|
-
this.cache.rollbackBatch();
|
|
1583
1538
|
if (err?.name === "AbortError") {
|
|
1584
1539
|
logger.debug("AWS EC2 CSV streaming timed out", { region });
|
|
1585
1540
|
} else {
|
|
@@ -2600,20 +2555,23 @@ var AzureRetailClient = class {
|
|
|
2600
2555
|
pickVmItem(items, vmSize, os) {
|
|
2601
2556
|
if (items.length === 0) return null;
|
|
2602
2557
|
const sorted = [...items].sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
|
|
2603
|
-
const
|
|
2558
|
+
const normalizeSku = (value) => value.toLowerCase().replace(/^standard[_\s-]*/, "").replace(/[^a-z0-9]/g, "");
|
|
2559
|
+
const requestedSku = normalizeSku(vmSize);
|
|
2604
2560
|
const isWindows = os.toLowerCase().includes("windows");
|
|
2605
2561
|
const onDemand = sorted.filter((item) => {
|
|
2606
2562
|
const meterLower = (item.meterName ?? "").toLowerCase();
|
|
2607
2563
|
return !meterLower.includes("low priority") && !meterLower.includes("spot");
|
|
2608
2564
|
});
|
|
2609
|
-
|
|
2565
|
+
const osMatches = onDemand.filter((item) => {
|
|
2610
2566
|
const skuLower = (item.skuName ?? "").toLowerCase();
|
|
2611
2567
|
const hasWindows = skuLower.includes("windows") || (item.productName ?? "").toLowerCase().includes("windows");
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2568
|
+
return isWindows && hasWindows || !isWindows && !hasWindows;
|
|
2569
|
+
});
|
|
2570
|
+
const exact = osMatches.find(
|
|
2571
|
+
(item) => normalizeSku(item.armSkuName ?? "") === requestedSku || normalizeSku(item.skuName ?? "") === requestedSku
|
|
2572
|
+
);
|
|
2573
|
+
if (exact) return exact;
|
|
2574
|
+
return osMatches[0] ?? onDemand[0] ?? sorted[0];
|
|
2617
2575
|
}
|
|
2618
2576
|
// -------------------------------------------------------------------------
|
|
2619
2577
|
// Internal helpers – size interpolation
|
|
@@ -2741,7 +2699,7 @@ function parseAzureVmSeries(vmName) {
|
|
|
2741
2699
|
}
|
|
2742
2700
|
|
|
2743
2701
|
// src/pricing/gcp/gcp-normalizer.ts
|
|
2744
|
-
function normalize3(resourceType, pricePerUnit, region, spec
|
|
2702
|
+
function normalize3(resourceType, pricePerUnit, region, spec) {
|
|
2745
2703
|
return {
|
|
2746
2704
|
provider: "gcp",
|
|
2747
2705
|
service: spec.service,
|
|
@@ -2753,10 +2711,9 @@ function normalize3(resourceType, pricePerUnit, region, spec, opts) {
|
|
|
2753
2711
|
description: `${spec.descriptionPrefix} ${resourceType}`,
|
|
2754
2712
|
attributes: {
|
|
2755
2713
|
[spec.attrKey]: resourceType,
|
|
2756
|
-
|
|
2757
|
-
pricing_source: opts.source
|
|
2714
|
+
pricing_source: "bundled"
|
|
2758
2715
|
},
|
|
2759
|
-
effective_date: resolveEffectiveDate(
|
|
2716
|
+
effective_date: resolveEffectiveDate(void 0)
|
|
2760
2717
|
};
|
|
2761
2718
|
}
|
|
2762
2719
|
var COMPUTE_SPEC = {
|
|
@@ -2784,16 +2741,16 @@ var DISK_SPEC = {
|
|
|
2784
2741
|
attrKey: "disk_type"
|
|
2785
2742
|
};
|
|
2786
2743
|
function normalizeGcpCompute(machineType, hourlyPrice, region) {
|
|
2787
|
-
return normalize3(machineType, hourlyPrice, region, COMPUTE_SPEC
|
|
2744
|
+
return normalize3(machineType, hourlyPrice, region, COMPUTE_SPEC);
|
|
2788
2745
|
}
|
|
2789
2746
|
function normalizeGcpDatabase(tier, hourlyPrice, region) {
|
|
2790
|
-
return normalize3(tier, hourlyPrice, region, DATABASE_SPEC
|
|
2747
|
+
return normalize3(tier, hourlyPrice, region, DATABASE_SPEC);
|
|
2791
2748
|
}
|
|
2792
2749
|
function normalizeGcpStorage(storageClass, pricePerGb, region) {
|
|
2793
|
-
return normalize3(storageClass, pricePerGb, region, STORAGE_SPEC
|
|
2750
|
+
return normalize3(storageClass, pricePerGb, region, STORAGE_SPEC);
|
|
2794
2751
|
}
|
|
2795
2752
|
function normalizeGcpDisk(diskType, pricePerGb, region) {
|
|
2796
|
-
return normalize3(diskType, pricePerGb, region, DISK_SPEC
|
|
2753
|
+
return normalize3(diskType, pricePerGb, region, DISK_SPEC);
|
|
2797
2754
|
}
|
|
2798
2755
|
|
|
2799
2756
|
// src/pricing/gcp/bundled-loader.ts
|
|
@@ -2983,524 +2940,6 @@ var GcpBundledLoader = class {
|
|
|
2983
2940
|
}
|
|
2984
2941
|
};
|
|
2985
2942
|
|
|
2986
|
-
// src/pricing/gcp/cloud-billing-client.ts
|
|
2987
|
-
var BILLING_API_BASE = "https://cloudbilling.googleapis.com/v1/services";
|
|
2988
|
-
var SERVICE_IDS = {
|
|
2989
|
-
COMPUTE: "6F81-5844-456A",
|
|
2990
|
-
CLOUD_SQL: "9662-B51E-5089",
|
|
2991
|
-
CLOUD_STORAGE: "95FF-2EF5-5EA1",
|
|
2992
|
-
// GKE (Kubernetes Engine) service — used to locate Autopilot pod SKUs.
|
|
2993
|
-
GKE: "CCD8-9BF1-090E"
|
|
2994
|
-
};
|
|
2995
|
-
var CACHE_TTL3 = 86400;
|
|
2996
|
-
function protoToFloat(units, nanos) {
|
|
2997
|
-
const u = parseInt(units ?? "0", 10) || 0;
|
|
2998
|
-
const n = nanos ?? 0;
|
|
2999
|
-
return u + n / 1e9;
|
|
3000
|
-
}
|
|
3001
|
-
function extractUnitPrice(sku) {
|
|
3002
|
-
const pricing = sku.pricingInfo?.[0];
|
|
3003
|
-
if (!pricing) return null;
|
|
3004
|
-
const rates = pricing.pricingExpression?.tieredRates ?? [];
|
|
3005
|
-
const baseTier = rates.find((r) => r.startUsageAmount === 0) ?? rates[0];
|
|
3006
|
-
if (!baseTier) return null;
|
|
3007
|
-
const price = protoToFloat(baseTier.unitPrice?.units, baseTier.unitPrice?.nanos);
|
|
3008
|
-
return price > 0 ? price : null;
|
|
3009
|
-
}
|
|
3010
|
-
function skuCoversRegion(sku, region) {
|
|
3011
|
-
const regions = sku.serviceRegions ?? [];
|
|
3012
|
-
const lower = region.toLowerCase();
|
|
3013
|
-
if (regions.includes("global")) return true;
|
|
3014
|
-
if (regions.some((r) => r.toLowerCase() === lower)) return true;
|
|
3015
|
-
const prefix = lower.split("-")[0];
|
|
3016
|
-
if (prefix && regions.some((r) => r.toLowerCase() === prefix)) return true;
|
|
3017
|
-
return false;
|
|
3018
|
-
}
|
|
3019
|
-
var CloudBillingClient = class {
|
|
3020
|
-
cache;
|
|
3021
|
-
/** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
|
|
3022
|
-
ttlSeconds;
|
|
3023
|
-
constructor(cache, ttlSeconds = CACHE_TTL3) {
|
|
3024
|
-
this.cache = cache;
|
|
3025
|
-
this.ttlSeconds = ttlSeconds;
|
|
3026
|
-
}
|
|
3027
|
-
// -------------------------------------------------------------------------
|
|
3028
|
-
// Public fetch methods
|
|
3029
|
-
// -------------------------------------------------------------------------
|
|
3030
|
-
/**
|
|
3031
|
-
* Fetch Compute Engine SKUs for a region and return the hourly instance
|
|
3032
|
-
* price for the given machine type, or null if not found.
|
|
3033
|
-
*/
|
|
3034
|
-
async fetchComputeSkus(machineType, region) {
|
|
3035
|
-
const cacheKey = `gcp/compute/${region}/${machineType.toLowerCase()}`;
|
|
3036
|
-
const cached = this.cache.get(cacheKey);
|
|
3037
|
-
if (cached) return cached;
|
|
3038
|
-
try {
|
|
3039
|
-
const skus = await this.fetchSkus(SERVICE_IDS.COMPUTE, region);
|
|
3040
|
-
const family = this.machineFamily(machineType);
|
|
3041
|
-
const result = this.extractComputePrice(skus, machineType, family, region);
|
|
3042
|
-
if (result) {
|
|
3043
|
-
this.cache.set(cacheKey, result, "gcp", "compute", region, this.ttlSeconds);
|
|
3044
|
-
return result;
|
|
3045
|
-
}
|
|
3046
|
-
} catch (err) {
|
|
3047
|
-
logger.warn("GCP Cloud Billing compute fetch failed", {
|
|
3048
|
-
machineType,
|
|
3049
|
-
region,
|
|
3050
|
-
err: err instanceof Error ? err.message : String(err)
|
|
3051
|
-
});
|
|
3052
|
-
}
|
|
3053
|
-
return null;
|
|
3054
|
-
}
|
|
3055
|
-
/**
|
|
3056
|
-
* Fetch Cloud SQL SKUs and return the hourly price for the given tier.
|
|
3057
|
-
*/
|
|
3058
|
-
async fetchDatabaseSkus(tier, region) {
|
|
3059
|
-
const cacheKey = `gcp/sql/${region}/${tier.toLowerCase()}`;
|
|
3060
|
-
const cached = this.cache.get(cacheKey);
|
|
3061
|
-
if (cached) return cached;
|
|
3062
|
-
try {
|
|
3063
|
-
const skus = await this.fetchSkus(SERVICE_IDS.CLOUD_SQL, region);
|
|
3064
|
-
const result = this.extractDatabasePrice(skus, tier, region);
|
|
3065
|
-
if (result) {
|
|
3066
|
-
this.cache.set(cacheKey, result, "gcp", "cloud-sql", region, this.ttlSeconds);
|
|
3067
|
-
return result;
|
|
3068
|
-
}
|
|
3069
|
-
} catch (err) {
|
|
3070
|
-
logger.warn("GCP Cloud Billing SQL fetch failed", {
|
|
3071
|
-
tier,
|
|
3072
|
-
region,
|
|
3073
|
-
err: err instanceof Error ? err.message : String(err)
|
|
3074
|
-
});
|
|
3075
|
-
}
|
|
3076
|
-
return null;
|
|
3077
|
-
}
|
|
3078
|
-
/**
|
|
3079
|
-
* Fetch Cloud Storage SKUs and return the monthly per-GB price for the
|
|
3080
|
-
* given storage class.
|
|
3081
|
-
*/
|
|
3082
|
-
async fetchStorageSkus(storageClass, region) {
|
|
3083
|
-
const cacheKey = `gcp/storage/${region}/${storageClass.toLowerCase()}`;
|
|
3084
|
-
const cached = this.cache.get(cacheKey);
|
|
3085
|
-
if (cached) return cached;
|
|
3086
|
-
try {
|
|
3087
|
-
const skus = await this.fetchSkus(SERVICE_IDS.CLOUD_STORAGE, region);
|
|
3088
|
-
const result = this.extractStoragePrice(skus, storageClass, region);
|
|
3089
|
-
if (result) {
|
|
3090
|
-
this.cache.set(cacheKey, result, "gcp", "cloud-storage", region, this.ttlSeconds);
|
|
3091
|
-
return result;
|
|
3092
|
-
}
|
|
3093
|
-
} catch (err) {
|
|
3094
|
-
logger.warn("GCP Cloud Billing storage fetch failed", {
|
|
3095
|
-
storageClass,
|
|
3096
|
-
region,
|
|
3097
|
-
err: err instanceof Error ? err.message : String(err)
|
|
3098
|
-
});
|
|
3099
|
-
}
|
|
3100
|
-
return null;
|
|
3101
|
-
}
|
|
3102
|
-
/**
|
|
3103
|
-
* Fetch Compute Engine GPU (accelerator) SKUs and return the hourly price
|
|
3104
|
-
* for the given accelerator type (e.g. "nvidia-tesla-t4", "nvidia-a100").
|
|
3105
|
-
*
|
|
3106
|
-
* GPUs live under the Compute service (6F81-5844-456A) with
|
|
3107
|
-
* `category.resourceFamily == "Compute"` and
|
|
3108
|
-
* `category.resourceGroup == "GPU"`. Descriptions follow the pattern
|
|
3109
|
-
* "Nvidia Tesla T4 GPU running in Americas".
|
|
3110
|
-
*/
|
|
3111
|
-
async fetchGpuSkus(accelerator, region) {
|
|
3112
|
-
const cacheKey = `gcp/gpu/${region}/${accelerator.toLowerCase()}`;
|
|
3113
|
-
const cached = this.cache.get(cacheKey);
|
|
3114
|
-
if (cached) return cached;
|
|
3115
|
-
try {
|
|
3116
|
-
const skus = await this.fetchSkus(SERVICE_IDS.COMPUTE, region);
|
|
3117
|
-
const result = this.extractGpuPrice(skus, accelerator, region);
|
|
3118
|
-
if (result) {
|
|
3119
|
-
this.cache.set(cacheKey, result, "gcp", "compute-gpu", region, this.ttlSeconds);
|
|
3120
|
-
return result;
|
|
3121
|
-
}
|
|
3122
|
-
} catch (err) {
|
|
3123
|
-
logger.warn("GCP Cloud Billing GPU fetch failed", {
|
|
3124
|
-
accelerator,
|
|
3125
|
-
region,
|
|
3126
|
-
err: err instanceof Error ? err.message : String(err)
|
|
3127
|
-
});
|
|
3128
|
-
}
|
|
3129
|
-
return null;
|
|
3130
|
-
}
|
|
3131
|
-
/**
|
|
3132
|
-
* Fetch Committed Use Discount (CUD) rates for the Compute service in a
|
|
3133
|
-
* region. Returns discount fractions (0..1) against OnDemand base rates,
|
|
3134
|
-
* derived by comparing matching Commit1Yr / Commit3Yr SKUs to their
|
|
3135
|
-
* OnDemand counterparts for the same resource group (e.g. "N1Standard").
|
|
3136
|
-
*
|
|
3137
|
-
* If the API call fails or no matching SKUs are found, returns null and
|
|
3138
|
-
* callers should fall back to static rates.
|
|
3139
|
-
*/
|
|
3140
|
-
async fetchCudRates(region) {
|
|
3141
|
-
const cacheKey = `gcp/cud/${region}`;
|
|
3142
|
-
const cached = this.cache.get(cacheKey);
|
|
3143
|
-
if (cached) return cached;
|
|
3144
|
-
try {
|
|
3145
|
-
const skus = await this.fetchSkus(SERVICE_IDS.COMPUTE, region);
|
|
3146
|
-
const onDemand = /* @__PURE__ */ new Map();
|
|
3147
|
-
const commit1 = /* @__PURE__ */ new Map();
|
|
3148
|
-
const commit3 = /* @__PURE__ */ new Map();
|
|
3149
|
-
const keyOf = (sku) => {
|
|
3150
|
-
const group = sku.category.resourceGroup ?? "";
|
|
3151
|
-
if (!group) return null;
|
|
3152
|
-
const desc = sku.description.toLowerCase();
|
|
3153
|
-
let flavour;
|
|
3154
|
-
if (desc.includes("core") || desc.includes("vcpu")) {
|
|
3155
|
-
flavour = "core";
|
|
3156
|
-
} else if (desc.includes("ram") || desc.includes("memory")) {
|
|
3157
|
-
flavour = "ram";
|
|
3158
|
-
} else {
|
|
3159
|
-
return null;
|
|
3160
|
-
}
|
|
3161
|
-
return `${group}|${flavour}`;
|
|
3162
|
-
};
|
|
3163
|
-
for (const sku of skus) {
|
|
3164
|
-
if (sku.category.resourceFamily !== "Compute") continue;
|
|
3165
|
-
const price = extractUnitPrice(sku);
|
|
3166
|
-
if (price === null) continue;
|
|
3167
|
-
const key = keyOf(sku);
|
|
3168
|
-
if (!key) continue;
|
|
3169
|
-
const usage = sku.category.usageType;
|
|
3170
|
-
if (usage === "OnDemand") {
|
|
3171
|
-
const existing = onDemand.get(key);
|
|
3172
|
-
if (existing === void 0 || price < existing) onDemand.set(key, price);
|
|
3173
|
-
} else if (usage === "Commit1Yr") {
|
|
3174
|
-
const existing = commit1.get(key);
|
|
3175
|
-
if (existing === void 0 || price < existing) commit1.set(key, price);
|
|
3176
|
-
} else if (usage === "Commit3Yr") {
|
|
3177
|
-
const existing = commit3.get(key);
|
|
3178
|
-
if (existing === void 0 || price < existing) commit3.set(key, price);
|
|
3179
|
-
}
|
|
3180
|
-
}
|
|
3181
|
-
const averageDiscount = (commits) => {
|
|
3182
|
-
const ratios = [];
|
|
3183
|
-
for (const [key, commitPrice] of commits) {
|
|
3184
|
-
const base = onDemand.get(key);
|
|
3185
|
-
if (base === void 0 || base <= 0) continue;
|
|
3186
|
-
const discount = 1 - commitPrice / base;
|
|
3187
|
-
if (discount > 0 && discount < 1) ratios.push(discount);
|
|
3188
|
-
}
|
|
3189
|
-
if (ratios.length === 0) return null;
|
|
3190
|
-
const sum = ratios.reduce((a, b) => a + b, 0);
|
|
3191
|
-
return sum / ratios.length;
|
|
3192
|
-
};
|
|
3193
|
-
const result = {
|
|
3194
|
-
term1yr: averageDiscount(commit1),
|
|
3195
|
-
term3yr: averageDiscount(commit3),
|
|
3196
|
-
sample_count: onDemand.size
|
|
3197
|
-
};
|
|
3198
|
-
if (result.term1yr === null && result.term3yr === null) {
|
|
3199
|
-
return null;
|
|
3200
|
-
}
|
|
3201
|
-
this.cache.set(cacheKey, result, "gcp", "compute-cud", region, this.ttlSeconds);
|
|
3202
|
-
return result;
|
|
3203
|
-
} catch (err) {
|
|
3204
|
-
logger.warn("GCP Cloud Billing CUD fetch failed", {
|
|
3205
|
-
region,
|
|
3206
|
-
err: err instanceof Error ? err.message : String(err)
|
|
3207
|
-
});
|
|
3208
|
-
return null;
|
|
3209
|
-
}
|
|
3210
|
-
}
|
|
3211
|
-
/**
|
|
3212
|
-
* Fetch GKE Autopilot pod resource rates (per-vCPU-hour, per-GB-memory-hour,
|
|
3213
|
-
* per-GB-ephemeral-storage-hour) from the GKE service catalog. Returns a
|
|
3214
|
-
* NormalizedPrice whose `price_per_unit` is the vCPU rate and whose
|
|
3215
|
-
* `attributes` carry the memory/storage rates as strings — callers that
|
|
3216
|
-
* know a pod's resource request can compute an exact per-pod price.
|
|
3217
|
-
*/
|
|
3218
|
-
async fetchAutopilotPodRates(region) {
|
|
3219
|
-
const cacheKey = `gcp/gke-autopilot/${region}`;
|
|
3220
|
-
const cached = this.cache.get(cacheKey);
|
|
3221
|
-
if (cached) return cached;
|
|
3222
|
-
try {
|
|
3223
|
-
const skus = await this.fetchSkus(SERVICE_IDS.GKE, region);
|
|
3224
|
-
const result = this.extractAutopilotPodRates(skus, region);
|
|
3225
|
-
if (result) {
|
|
3226
|
-
this.cache.set(cacheKey, result, "gcp", "gke-autopilot", region, this.ttlSeconds);
|
|
3227
|
-
return result;
|
|
3228
|
-
}
|
|
3229
|
-
} catch (err) {
|
|
3230
|
-
logger.warn("GCP Cloud Billing GKE Autopilot fetch failed", {
|
|
3231
|
-
region,
|
|
3232
|
-
err: err instanceof Error ? err.message : String(err)
|
|
3233
|
-
});
|
|
3234
|
-
}
|
|
3235
|
-
return null;
|
|
3236
|
-
}
|
|
3237
|
-
// -------------------------------------------------------------------------
|
|
3238
|
-
// Internal – API fetch
|
|
3239
|
-
// -------------------------------------------------------------------------
|
|
3240
|
-
/**
|
|
3241
|
-
* Page through all SKUs for a GCP billing service, filtering to those that
|
|
3242
|
-
* cover the requested region. Returns the filtered SKU array.
|
|
3243
|
-
*
|
|
3244
|
-
* The public endpoint supports no authentication for read-only SKU listing.
|
|
3245
|
-
* Pagination is handled via nextPageToken.
|
|
3246
|
-
*/
|
|
3247
|
-
async fetchSkus(serviceId, region) {
|
|
3248
|
-
const url = `${BILLING_API_BASE}/${serviceId}/skus`;
|
|
3249
|
-
const collected = [];
|
|
3250
|
-
let pageToken;
|
|
3251
|
-
do {
|
|
3252
|
-
const pageUrl = pageToken ? `${url}?pageToken=${pageToken}` : url;
|
|
3253
|
-
logger.debug("GCP Cloud Billing API request", { url: pageUrl, serviceId, region });
|
|
3254
|
-
const res = await fetchWithRetryAndCircuitBreaker(
|
|
3255
|
-
pageUrl,
|
|
3256
|
-
{
|
|
3257
|
-
signal: AbortSignal.timeout(3e4),
|
|
3258
|
-
headers: { Accept: "application/json" }
|
|
3259
|
-
},
|
|
3260
|
-
{ maxRetries: 2 }
|
|
3261
|
-
);
|
|
3262
|
-
if (!res.ok) {
|
|
3263
|
-
throw new Error(`GCP Billing API HTTP ${res.status} for service ${serviceId}`);
|
|
3264
|
-
}
|
|
3265
|
-
const body = await res.json();
|
|
3266
|
-
const skus = body.skus ?? [];
|
|
3267
|
-
for (const sku of skus) {
|
|
3268
|
-
if (skuCoversRegion(sku, region)) {
|
|
3269
|
-
collected.push(sku);
|
|
3270
|
-
}
|
|
3271
|
-
}
|
|
3272
|
-
pageToken = body.nextPageToken;
|
|
3273
|
-
} while (pageToken);
|
|
3274
|
-
logger.debug("GCP Cloud Billing SKUs fetched", {
|
|
3275
|
-
serviceId,
|
|
3276
|
-
region,
|
|
3277
|
-
count: collected.length
|
|
3278
|
-
});
|
|
3279
|
-
return collected;
|
|
3280
|
-
}
|
|
3281
|
-
// -------------------------------------------------------------------------
|
|
3282
|
-
// Internal – SKU matching helpers
|
|
3283
|
-
// -------------------------------------------------------------------------
|
|
3284
|
-
/**
|
|
3285
|
-
* Extract the machine family prefix from a machine type string.
|
|
3286
|
-
* "n2-standard-4" → "n2"
|
|
3287
|
-
* "e2-micro" → "e2"
|
|
3288
|
-
* "c2-standard-8" → "c2"
|
|
3289
|
-
*/
|
|
3290
|
-
machineFamily(machineType) {
|
|
3291
|
-
return machineType.split("-")[0]?.toLowerCase() ?? machineType.toLowerCase();
|
|
3292
|
-
}
|
|
3293
|
-
/**
|
|
3294
|
-
* From a list of Compute Engine SKUs, find the best price for the given
|
|
3295
|
-
* machine type. The API does not have per-instance-type SKUs; instead it
|
|
3296
|
-
* has per-resource (vCPU, RAM) SKUs per machine family. This method finds
|
|
3297
|
-
* the core/vCPU hourly SKU for the family and returns it as a proxy for the
|
|
3298
|
-
* full instance price (the bundled loader provides the assembled price).
|
|
3299
|
-
*
|
|
3300
|
-
* We return the first matching SKU's unit price tagged with `pricing_source:
|
|
3301
|
-
* "live"` so callers can distinguish from bundled data.
|
|
3302
|
-
*/
|
|
3303
|
-
extractComputePrice(skus, machineType, family, region) {
|
|
3304
|
-
const familyUpper = family.toUpperCase();
|
|
3305
|
-
const candidates = skus.filter((s) => {
|
|
3306
|
-
const desc = s.description.toUpperCase();
|
|
3307
|
-
return desc.includes(familyUpper) && s.category.resourceFamily === "Compute" && s.category.usageType === "OnDemand" && // We want vCPU-hour SKUs as the representative price signal
|
|
3308
|
-
(desc.includes("CORE") || desc.includes("VCPU") || desc.includes("INSTANCE CORE"));
|
|
3309
|
-
});
|
|
3310
|
-
if (candidates.length === 0) return null;
|
|
3311
|
-
candidates.sort((a, b) => a.skuId.localeCompare(b.skuId));
|
|
3312
|
-
const sku = candidates[0];
|
|
3313
|
-
const price = extractUnitPrice(sku);
|
|
3314
|
-
if (price === null) return null;
|
|
3315
|
-
return {
|
|
3316
|
-
provider: "gcp",
|
|
3317
|
-
service: "compute-engine",
|
|
3318
|
-
resource_type: machineType,
|
|
3319
|
-
region,
|
|
3320
|
-
unit: "h",
|
|
3321
|
-
price_per_unit: price,
|
|
3322
|
-
currency: "USD",
|
|
3323
|
-
description: `GCP Compute Engine ${machineType} (live: ${sku.description})`,
|
|
3324
|
-
attributes: {
|
|
3325
|
-
machine_type: machineType,
|
|
3326
|
-
machine_family: family,
|
|
3327
|
-
sku_id: sku.skuId,
|
|
3328
|
-
pricing_source: "live"
|
|
3329
|
-
},
|
|
3330
|
-
effective_date: sku.pricingInfo?.[0]?.effectiveTime ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3331
|
-
};
|
|
3332
|
-
}
|
|
3333
|
-
/**
|
|
3334
|
-
* Extract a Cloud SQL hourly price for the given tier.
|
|
3335
|
-
* Cloud SQL SKU descriptions contain the tier class information:
|
|
3336
|
-
* e.g. "DB custom CORE running in Americas"
|
|
3337
|
-
*/
|
|
3338
|
-
extractDatabasePrice(skus, tier, region) {
|
|
3339
|
-
const tierLower = tier.toLowerCase();
|
|
3340
|
-
const candidates = skus.filter((s) => {
|
|
3341
|
-
const desc = s.description.toLowerCase();
|
|
3342
|
-
return s.category.resourceFamily === "ApplicationServices" && s.category.usageType === "OnDemand" && (desc.includes("core") || desc.includes("vcpu")) && // Exclude storage SKUs
|
|
3343
|
-
!desc.includes("storage") && !desc.includes("io") && !desc.includes("network");
|
|
3344
|
-
});
|
|
3345
|
-
if (candidates.length === 0) return null;
|
|
3346
|
-
candidates.sort((a, b) => a.skuId.localeCompare(b.skuId));
|
|
3347
|
-
const sku = candidates[0];
|
|
3348
|
-
const price = extractUnitPrice(sku);
|
|
3349
|
-
if (price === null) return null;
|
|
3350
|
-
return {
|
|
3351
|
-
provider: "gcp",
|
|
3352
|
-
service: "cloud-sql",
|
|
3353
|
-
resource_type: tier,
|
|
3354
|
-
region,
|
|
3355
|
-
unit: "h",
|
|
3356
|
-
price_per_unit: price,
|
|
3357
|
-
currency: "USD",
|
|
3358
|
-
description: `GCP Cloud SQL ${tier} (live: ${sku.description})`,
|
|
3359
|
-
attributes: {
|
|
3360
|
-
tier: tierLower,
|
|
3361
|
-
sku_id: sku.skuId,
|
|
3362
|
-
pricing_source: "live"
|
|
3363
|
-
},
|
|
3364
|
-
effective_date: sku.pricingInfo?.[0]?.effectiveTime ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3365
|
-
};
|
|
3366
|
-
}
|
|
3367
|
-
/**
|
|
3368
|
-
* Extract a Cloud Storage per-GB monthly price for the given storage class.
|
|
3369
|
-
*/
|
|
3370
|
-
extractStoragePrice(skus, storageClass, region) {
|
|
3371
|
-
const classUpper = storageClass.toUpperCase();
|
|
3372
|
-
const classKeywords = {
|
|
3373
|
-
STANDARD: ["standard storage"],
|
|
3374
|
-
NEARLINE: ["nearline storage"],
|
|
3375
|
-
COLDLINE: ["coldline storage"],
|
|
3376
|
-
ARCHIVE: ["archive storage"]
|
|
3377
|
-
};
|
|
3378
|
-
const keywords = classKeywords[classUpper] ?? [classUpper.toLowerCase() + " storage"];
|
|
3379
|
-
const candidates = skus.filter((s) => {
|
|
3380
|
-
const desc = s.description.toLowerCase();
|
|
3381
|
-
return s.category.resourceFamily === "Storage" && s.category.usageType === "OnDemand" && keywords.some((kw) => desc.includes(kw)) && !desc.includes("retrieval") && !desc.includes("operation") && !desc.includes("network");
|
|
3382
|
-
});
|
|
3383
|
-
if (candidates.length === 0) return null;
|
|
3384
|
-
candidates.sort((a, b) => a.skuId.localeCompare(b.skuId));
|
|
3385
|
-
const sku = candidates[0];
|
|
3386
|
-
const price = extractUnitPrice(sku);
|
|
3387
|
-
if (price === null) return null;
|
|
3388
|
-
return {
|
|
3389
|
-
provider: "gcp",
|
|
3390
|
-
service: "cloud-storage",
|
|
3391
|
-
resource_type: classUpper,
|
|
3392
|
-
region,
|
|
3393
|
-
unit: "GiBy.mo",
|
|
3394
|
-
price_per_unit: price,
|
|
3395
|
-
currency: "USD",
|
|
3396
|
-
description: `GCP Cloud Storage ${classUpper} (live: ${sku.description})`,
|
|
3397
|
-
attributes: {
|
|
3398
|
-
storage_class: classUpper,
|
|
3399
|
-
sku_id: sku.skuId,
|
|
3400
|
-
pricing_source: "live"
|
|
3401
|
-
},
|
|
3402
|
-
effective_date: sku.pricingInfo?.[0]?.effectiveTime ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3403
|
-
};
|
|
3404
|
-
}
|
|
3405
|
-
/**
|
|
3406
|
-
* Extract a GPU hourly price for the requested accelerator type.
|
|
3407
|
-
* Matches SKUs with resourceGroup == "GPU" whose description contains the
|
|
3408
|
-
* human-readable model name (e.g. "Tesla T4", "A100 40GB").
|
|
3409
|
-
*/
|
|
3410
|
-
extractGpuPrice(skus, accelerator, region) {
|
|
3411
|
-
const lower = accelerator.toLowerCase();
|
|
3412
|
-
const stripped = lower.replace(/^nvidia-?/, "");
|
|
3413
|
-
const tokens = stripped.split(/[-\s]+/).filter((t) => t.length > 0);
|
|
3414
|
-
if (tokens.length === 0) return null;
|
|
3415
|
-
const candidates = skus.filter((s) => {
|
|
3416
|
-
if (s.category.resourceFamily !== "Compute") return false;
|
|
3417
|
-
if (s.category.resourceGroup !== "GPU") return false;
|
|
3418
|
-
if (s.category.usageType !== "OnDemand") return false;
|
|
3419
|
-
const desc = s.description.toLowerCase();
|
|
3420
|
-
return tokens.every((tok) => desc.includes(tok));
|
|
3421
|
-
});
|
|
3422
|
-
if (candidates.length === 0) return null;
|
|
3423
|
-
candidates.sort((a, b) => a.skuId.localeCompare(b.skuId));
|
|
3424
|
-
const sku = candidates[0];
|
|
3425
|
-
const price = extractUnitPrice(sku);
|
|
3426
|
-
if (price === null) return null;
|
|
3427
|
-
return {
|
|
3428
|
-
provider: "gcp",
|
|
3429
|
-
service: "compute-engine",
|
|
3430
|
-
resource_type: accelerator,
|
|
3431
|
-
region,
|
|
3432
|
-
unit: "h",
|
|
3433
|
-
price_per_unit: price,
|
|
3434
|
-
currency: "USD",
|
|
3435
|
-
description: `GCP GPU ${accelerator} (live: ${sku.description})`,
|
|
3436
|
-
attributes: {
|
|
3437
|
-
accelerator,
|
|
3438
|
-
sku_id: sku.skuId,
|
|
3439
|
-
pricing_source: "live"
|
|
3440
|
-
},
|
|
3441
|
-
effective_date: sku.pricingInfo?.[0]?.effectiveTime ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3442
|
-
};
|
|
3443
|
-
}
|
|
3444
|
-
/**
|
|
3445
|
-
* Extract GKE Autopilot pod resource rates. Autopilot exposes separate
|
|
3446
|
-
* SKUs per resource dimension (vCPU, memory, ephemeral storage) under the
|
|
3447
|
-
* GKE service. We look for SKUs whose description contains "Autopilot"
|
|
3448
|
-
* and the matching dimension keyword.
|
|
3449
|
-
*/
|
|
3450
|
-
extractAutopilotPodRates(skus, region) {
|
|
3451
|
-
const autopilotSkus = skus.filter((s) => {
|
|
3452
|
-
const desc = s.description.toLowerCase();
|
|
3453
|
-
return s.category.usageType === "OnDemand" && desc.includes("autopilot") && !desc.includes("spot") && !desc.includes("preemptible");
|
|
3454
|
-
});
|
|
3455
|
-
if (autopilotSkus.length === 0) return null;
|
|
3456
|
-
const find = (keywords) => {
|
|
3457
|
-
const matches = autopilotSkus.filter((s) => {
|
|
3458
|
-
const desc = s.description.toLowerCase();
|
|
3459
|
-
return keywords.every((kw) => desc.includes(kw));
|
|
3460
|
-
});
|
|
3461
|
-
if (matches.length === 0) return void 0;
|
|
3462
|
-
matches.sort((a, b) => a.skuId.localeCompare(b.skuId));
|
|
3463
|
-
return matches[0];
|
|
3464
|
-
};
|
|
3465
|
-
const vcpuSku = find(["cpu"]) ?? find(["core"]) ?? find(["vcpu"]);
|
|
3466
|
-
const memorySku = find(["memory"]) ?? find(["ram"]);
|
|
3467
|
-
const storageSku = find(["ephemeral", "storage"]) ?? find(["storage"]);
|
|
3468
|
-
if (!vcpuSku) return null;
|
|
3469
|
-
const vcpuPrice = extractUnitPrice(vcpuSku);
|
|
3470
|
-
if (vcpuPrice === null) return null;
|
|
3471
|
-
const memoryPrice = memorySku ? extractUnitPrice(memorySku) : null;
|
|
3472
|
-
const storagePrice = storageSku ? extractUnitPrice(storageSku) : null;
|
|
3473
|
-
const attributes = {
|
|
3474
|
-
mode: "autopilot",
|
|
3475
|
-
pricing_model: "per_pod_resource",
|
|
3476
|
-
pricing_source: "live",
|
|
3477
|
-
vcpu_hourly: String(vcpuPrice),
|
|
3478
|
-
sku_id_vcpu: vcpuSku.skuId,
|
|
3479
|
-
pricing_note: "Autopilot charges per pod: vCPU, memory, and ephemeral storage are billed separately. price_per_unit reports the vCPU/hr rate; see attributes for memory/storage rates."
|
|
3480
|
-
};
|
|
3481
|
-
if (memoryPrice !== null && memorySku) {
|
|
3482
|
-
attributes.memory_gb_hourly = String(memoryPrice);
|
|
3483
|
-
attributes.sku_id_memory = memorySku.skuId;
|
|
3484
|
-
}
|
|
3485
|
-
if (storagePrice !== null && storageSku) {
|
|
3486
|
-
attributes.ephemeral_storage_gb_hourly = String(storagePrice);
|
|
3487
|
-
attributes.sku_id_storage = storageSku.skuId;
|
|
3488
|
-
}
|
|
3489
|
-
return {
|
|
3490
|
-
provider: "gcp",
|
|
3491
|
-
service: "gke",
|
|
3492
|
-
resource_type: "autopilot-pod",
|
|
3493
|
-
region,
|
|
3494
|
-
unit: "h",
|
|
3495
|
-
price_per_unit: vcpuPrice,
|
|
3496
|
-
currency: "USD",
|
|
3497
|
-
description: `GCP GKE Autopilot pod resources (live: ${vcpuSku.description})`,
|
|
3498
|
-
attributes,
|
|
3499
|
-
effective_date: vcpuSku.pricingInfo?.[0]?.effectiveTime ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3500
|
-
};
|
|
3501
|
-
}
|
|
3502
|
-
};
|
|
3503
|
-
|
|
3504
2943
|
// src/pricing/pricing-engine.ts
|
|
3505
2944
|
var DEFAULT_CACHE_TTL_SECONDS = 86400;
|
|
3506
2945
|
var AwsProvider = class {
|
|
@@ -3585,51 +3024,19 @@ var AzureProvider = class {
|
|
|
3585
3024
|
};
|
|
3586
3025
|
var GcpProvider = class {
|
|
3587
3026
|
loader;
|
|
3588
|
-
|
|
3589
|
-
constructor(cache, ttlSeconds) {
|
|
3027
|
+
constructor(_cache, _ttlSeconds) {
|
|
3590
3028
|
this.loader = new GcpBundledLoader();
|
|
3591
|
-
this.liveClient = new CloudBillingClient(cache, ttlSeconds);
|
|
3592
3029
|
}
|
|
3593
|
-
|
|
3594
|
-
try {
|
|
3595
|
-
const live = await this.liveClient.fetchComputeSkus(instanceType, region);
|
|
3596
|
-
if (live) return live;
|
|
3597
|
-
} catch (err) {
|
|
3598
|
-
logger.warn("GCP live compute pricing failed, falling back to bundled", {
|
|
3599
|
-
instanceType,
|
|
3600
|
-
region,
|
|
3601
|
-
err: err instanceof Error ? err.message : String(err)
|
|
3602
|
-
});
|
|
3603
|
-
}
|
|
3030
|
+
getComputePrice(instanceType, region, _os) {
|
|
3604
3031
|
return this.loader.getComputePrice(instanceType, region);
|
|
3605
3032
|
}
|
|
3606
|
-
|
|
3607
|
-
try {
|
|
3608
|
-
const live = await this.liveClient.fetchDatabaseSkus(instanceType, region);
|
|
3609
|
-
if (live) return live;
|
|
3610
|
-
} catch (err) {
|
|
3611
|
-
logger.warn("GCP live database pricing failed, falling back to bundled", {
|
|
3612
|
-
instanceType,
|
|
3613
|
-
region,
|
|
3614
|
-
err: err instanceof Error ? err.message : String(err)
|
|
3615
|
-
});
|
|
3616
|
-
}
|
|
3033
|
+
getDatabasePrice(instanceType, region, _engine) {
|
|
3617
3034
|
return this.loader.getDatabasePrice(instanceType, region);
|
|
3618
3035
|
}
|
|
3619
|
-
|
|
3036
|
+
getStoragePrice(storageType, region, _sizeGb) {
|
|
3620
3037
|
if (storageType.startsWith("pd-")) {
|
|
3621
3038
|
return this.loader.getDiskPrice(storageType, region);
|
|
3622
3039
|
}
|
|
3623
|
-
try {
|
|
3624
|
-
const live = await this.liveClient.fetchStorageSkus(storageType, region);
|
|
3625
|
-
if (live) return live;
|
|
3626
|
-
} catch (err) {
|
|
3627
|
-
logger.warn("GCP live storage pricing failed, falling back to bundled", {
|
|
3628
|
-
storageType,
|
|
3629
|
-
region,
|
|
3630
|
-
err: err instanceof Error ? err.message : String(err)
|
|
3631
|
-
});
|
|
3632
|
-
}
|
|
3633
3040
|
return this.loader.getStoragePrice(storageType, region);
|
|
3634
3041
|
}
|
|
3635
3042
|
getLoadBalancerPrice(_type, region) {
|
|
@@ -6311,7 +5718,7 @@ async function calculateComputeCost(resource, targetProvider, targetRegion, pric
|
|
|
6311
5718
|
`No instance mapping found for ${sourceInstanceType} (${resource.provider} -> ${targetProvider})`
|
|
6312
5719
|
);
|
|
6313
5720
|
} else {
|
|
6314
|
-
const { getInstanceMap: getInstanceMap2 } = await import("./loader-
|
|
5721
|
+
const { getInstanceMap: getInstanceMap2 } = await import("./loader-G7WW74B6.js");
|
|
6315
5722
|
const im = getInstanceMap2();
|
|
6316
5723
|
const dirKey = `${resource.provider}_to_${targetProvider}`;
|
|
6317
5724
|
const directMap = im[dirKey];
|
|
@@ -8947,9 +8354,11 @@ var CostEngine = class {
|
|
|
8947
8354
|
estimates.push(estimate);
|
|
8948
8355
|
const svc = serviceLabel(resource.type);
|
|
8949
8356
|
byService[svc] = (byService[svc] ?? 0) + estimate.monthly_cost;
|
|
8950
|
-
if (estimate.pricing_source === "fallback") {
|
|
8357
|
+
if (estimate.pricing_source === "fallback" || estimate.pricing_source === "bundled") {
|
|
8358
|
+
const meta = getPricingMetadata(estimate.provider);
|
|
8359
|
+
const vintage = meta.as_of !== null ? ` (as of ${meta.as_of}${meta.age_days !== null ? `, ${meta.age_days} days old` : ""}; ${meta.staleness})` : ` (vintage unknown, treated as stale)`;
|
|
8951
8360
|
warnings.push(
|
|
8952
|
-
`${estimate.resource_name} (${estimate.resource_type}):
|
|
8361
|
+
`${estimate.resource_name} (${estimate.resource_type}): priced from ${estimate.pricing_source} data, not live pricing${vintage}`
|
|
8953
8362
|
);
|
|
8954
8363
|
}
|
|
8955
8364
|
if (estimate.monthly_cost === 0 && estimate.confidence === "low") {
|
|
@@ -10193,4 +9602,4 @@ export {
|
|
|
10193
9602
|
whatIfSchema,
|
|
10194
9603
|
whatIf
|
|
10195
9604
|
};
|
|
10196
|
-
//# sourceMappingURL=chunk-
|
|
9605
|
+
//# sourceMappingURL=chunk-4P3I7BSL.js.map
|