@agent-finops/core 0.6.0 → 0.6.1

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.
@@ -20,6 +20,15 @@ export type ProviderQaPagination = {
20
20
  note?: string;
21
21
  };
22
22
  export type ProviderCoverageStatus = "complete" | "partial";
23
+ /**
24
+ * Exact provider interval requested by a sync. It is intentionally absent
25
+ * when the caller leaves the end open: a successful narrow/open-ended read
26
+ * must never be promoted into an assumed 30-day coverage claim.
27
+ */
28
+ export type ProviderCoverageInterval = {
29
+ coverageStart: string;
30
+ coverageEnd: string;
31
+ };
23
32
  export type ProviderFinancialSummary = {
24
33
  providerReportedBilledUsd: number | null;
25
34
  apiEquivalentEstimatedUsd: number | null;
@@ -65,6 +74,7 @@ export type ProviderConnectorResult = {
65
74
  records: UsageRecord[];
66
75
  fetchedAt: string;
67
76
  coverage: ProviderCoverageStatus;
77
+ coverageInterval?: ProviderCoverageInterval;
68
78
  financials: ProviderFinancialSummary;
69
79
  completeness: "verified" | "estimated" | "detected_unverified" | "missing";
70
80
  qa: ProviderQaSummary;
@@ -314,6 +314,12 @@ export function normalizeCursorSpendResponse(response, options) {
314
314
  });
315
315
  }
316
316
  export async function fetchProviderUsageRecords(input) {
317
+ // Validate explicit bounds before resolving a credential or making a request.
318
+ // The interval-aware OpenAI/Anthropic result paths call the same pure helper
319
+ // to attach normalized bounds only after a successful fetch. Copilot and
320
+ // Cursor do not currently constrain their reads to these requested bounds,
321
+ // so they deliberately return no coverage interval.
322
+ requestedCoverageInterval(input);
317
323
  const token = (input.tokenResolver ?? defaultTokenResolver)(input.authReference);
318
324
  const fetcher = input.fetcher ?? defaultFetcher;
319
325
  const sourceId = input.sourceId ?? `${input.provider}-provider-api`;
@@ -400,7 +406,7 @@ async function fetchOpenAi(input, token, fetcher, sourceId) {
400
406
  ...costFetch.pages.flatMap((page) => normalizeOpenAiCostResponse(page, { sourceId, observedFrom: "OpenAI organization costs API" })),
401
407
  ...usageFetch.pages.flatMap((page) => normalizeOpenAiUsageResponse(page, { sourceId, observedFrom: "OpenAI organization usage API" }))
402
408
  ];
403
- return providerResult("openai", sourceId, input.authReference, records, qaSummary("openai", [costFetch, usageFetch]));
409
+ return providerResult("openai", sourceId, input.authReference, records, qaSummary("openai", [costFetch, usageFetch]), requestedCoverageInterval(input));
404
410
  }
405
411
  async function fetchAnthropic(input, token, fetcher, sourceId) {
406
412
  const costRequest = {
@@ -417,7 +423,7 @@ async function fetchAnthropic(input, token, fetcher, sourceId) {
417
423
  ...costFetch.pages.flatMap((page) => normalizeAnthropicCostResponse(page, { sourceId, observedFrom: "Anthropic Admin Cost Report" })),
418
424
  ...claudeCodeFetches.flatMap((fetchResult) => fetchResult.pages.flatMap((page) => normalizeAnthropicClaudeCodeUsageResponse(page, { sourceId, observedFrom: "Anthropic Claude Code Usage Report", accountId: input.accountId })))
419
425
  ];
420
- return providerResult("anthropic", sourceId, input.authReference, records, qaSummary("anthropic", [costFetch, ...claudeCodeFetches]));
426
+ return providerResult("anthropic", sourceId, input.authReference, records, qaSummary("anthropic", [costFetch, ...claudeCodeFetches]), requestedCoverageInterval(input));
421
427
  }
422
428
  async function fetchGitHubCopilot(input, token, fetcher, sourceId) {
423
429
  const accountId = input.org ?? input.enterprise;
@@ -1257,7 +1263,7 @@ function sumAmounts(records) {
1257
1263
  .filter((amount) => typeof amount === "number");
1258
1264
  return amounts.length > 0 ? amounts.reduce((sum, amount) => sum + amount, 0) : null;
1259
1265
  }
1260
- function providerResult(provider, sourceId, authReference, records, qa) {
1266
+ function providerResult(provider, sourceId, authReference, records, qa, coverageInterval) {
1261
1267
  const resolvedQa = qa ?? qaSummary(provider, []);
1262
1268
  const coverage = resolvedQa.coverage
1263
1269
  ?? (resolvedQa.pagination.every((pagination) => pagination.stoppedBecause === "complete") ? "complete" : "partial");
@@ -1276,11 +1282,42 @@ function providerResult(provider, sourceId, authReference, records, qa) {
1276
1282
  records,
1277
1283
  fetchedAt: new Date().toISOString(),
1278
1284
  coverage,
1285
+ ...(coverageInterval ? { coverageInterval } : {}),
1279
1286
  financials,
1280
1287
  completeness,
1281
1288
  qa: resolvedQa
1282
1289
  };
1283
1290
  }
1291
+ function requestedCoverageInterval(input) {
1292
+ if (!Number.isFinite(input.startTime) || !Number.isInteger(input.startTime) || input.startTime < 0) {
1293
+ throw new Error("Provider coverage startTime requires a non-negative whole-second timestamp.");
1294
+ }
1295
+ const coverageStart = new Date(input.startTime * 1_000);
1296
+ if (Number.isNaN(coverageStart.getTime())) {
1297
+ throw new Error("Provider coverage interval falls outside the supported timestamp range.");
1298
+ }
1299
+ if (input.endTime === undefined) {
1300
+ if (coverageStart.getTime() > Date.now()) {
1301
+ throw new Error("Provider coverage startTime cannot be in the future.");
1302
+ }
1303
+ return undefined;
1304
+ }
1305
+ if (!Number.isFinite(input.endTime) || !Number.isInteger(input.endTime) ||
1306
+ input.endTime < input.startTime) {
1307
+ throw new Error("Provider coverage interval requires non-negative whole-second bounds with endTime at or after startTime.");
1308
+ }
1309
+ const coverageEnd = new Date(input.endTime * 1_000);
1310
+ if (Number.isNaN(coverageEnd.getTime())) {
1311
+ throw new Error("Provider coverage interval falls outside the supported timestamp range.");
1312
+ }
1313
+ if (coverageEnd.getTime() > Date.now()) {
1314
+ throw new Error("Provider coverage endTime cannot be in the future.");
1315
+ }
1316
+ return {
1317
+ coverageStart: coverageStart.toISOString(),
1318
+ coverageEnd: coverageEnd.toISOString()
1319
+ };
1320
+ }
1284
1321
  export function createProviderConnection(input) {
1285
1322
  const source = createProviderConnectorStub(input.provider, "provider_api", input.fetchedAt);
1286
1323
  const total = input.totalUsd === null ? "an unavailable financial headline" : formatProviderUsd(input.totalUsd);
@@ -1328,7 +1365,7 @@ function buildOpenAiCostsUrl(startTime, endTime) {
1328
1365
  url.searchParams.append("group_by", "project_id");
1329
1366
  url.searchParams.append("group_by", "line_item");
1330
1367
  url.searchParams.append("group_by", "api_key_id");
1331
- if (endTime)
1368
+ if (endTime !== undefined)
1332
1369
  url.searchParams.set("end_time", String(endTime));
1333
1370
  return url.toString();
1334
1371
  }
@@ -1341,14 +1378,14 @@ function buildOpenAiUsageUrl(startTime, endTime) {
1341
1378
  url.searchParams.append("group_by", "user_id");
1342
1379
  url.searchParams.append("group_by", "api_key_id");
1343
1380
  url.searchParams.append("group_by", "model");
1344
- if (endTime)
1381
+ if (endTime !== undefined)
1345
1382
  url.searchParams.set("end_time", String(endTime));
1346
1383
  return url.toString();
1347
1384
  }
1348
1385
  function buildAnthropicCostUrl(startTime, endTime) {
1349
1386
  const url = new URL("https://api.anthropic.com/v1/organizations/cost_report");
1350
1387
  url.searchParams.set("starting_at", new Date(startTime * 1000).toISOString());
1351
- if (endTime)
1388
+ if (endTime !== undefined)
1352
1389
  url.searchParams.set("ending_at", new Date(endTime * 1000).toISOString());
1353
1390
  url.searchParams.set("bucket_width", "1d");
1354
1391
  url.searchParams.append("group_by[]", "workspace_id");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-finops/core",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",