@agent-finops/core 0.6.0 → 0.7.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.
@@ -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/dist/schema.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
2
  export declare const costConfidenceValues: readonly ["verified", "estimated", "detected_unverified", "missing"];
3
3
  export declare const costConfidenceSchema: z.ZodEnum<{
4
- estimated: "estimated";
5
4
  verified: "verified";
5
+ estimated: "estimated";
6
6
  detected_unverified: "detected_unverified";
7
7
  missing: "missing";
8
8
  }>;
@@ -12,8 +12,8 @@ export declare const spendSourceSchema: z.ZodObject<{
12
12
  name: z.ZodString;
13
13
  provider: z.ZodString;
14
14
  confidence: z.ZodEnum<{
15
- estimated: "estimated";
16
15
  verified: "verified";
16
+ estimated: "estimated";
17
17
  detected_unverified: "detected_unverified";
18
18
  missing: "missing";
19
19
  }>;
@@ -59,8 +59,8 @@ export declare const usageRecordSchema: z.ZodObject<{
59
59
  name: z.ZodString;
60
60
  provider: z.ZodString;
61
61
  confidence: z.ZodEnum<{
62
- estimated: "estimated";
63
62
  verified: "verified";
63
+ estimated: "estimated";
64
64
  detected_unverified: "detected_unverified";
65
65
  missing: "missing";
66
66
  }>;
@@ -71,8 +71,8 @@ export declare const usageRecordSchema: z.ZodObject<{
71
71
  outputTokens: z.ZodNumber;
72
72
  amountUsd: z.ZodNullable<z.ZodNumber>;
73
73
  costConfidence: z.ZodEnum<{
74
- estimated: "estimated";
75
74
  verified: "verified";
75
+ estimated: "estimated";
76
76
  detected_unverified: "detected_unverified";
77
77
  missing: "missing";
78
78
  }>;
@@ -134,10 +134,10 @@ export declare function downgradeSampleUsageEvidence(records: UsageRecord[]): Us
134
134
  export declare function spendComparisonKey(record: UsageRecord): string | undefined;
135
135
  export declare const attributionCandidateSchema: z.ZodObject<{
136
136
  entityType: z.ZodEnum<{
137
- user: "user";
137
+ client: "client";
138
138
  project: "project";
139
139
  agent: "agent";
140
- client: "client";
140
+ user: "user";
141
141
  workspace: "workspace";
142
142
  api_key: "api_key";
143
143
  }>;
@@ -150,10 +150,10 @@ export declare const attributionMappingSchema: z.ZodObject<{
150
150
  usageRecordId: z.ZodString;
151
151
  candidates: z.ZodArray<z.ZodObject<{
152
152
  entityType: z.ZodEnum<{
153
- user: "user";
153
+ client: "client";
154
154
  project: "project";
155
155
  agent: "agent";
156
- client: "client";
156
+ user: "user";
157
157
  workspace: "workspace";
158
158
  api_key: "api_key";
159
159
  }>;
@@ -163,10 +163,10 @@ export declare const attributionMappingSchema: z.ZodObject<{
163
163
  }, z.core.$strip>>;
164
164
  selected: z.ZodOptional<z.ZodObject<{
165
165
  entityType: z.ZodEnum<{
166
- user: "user";
166
+ client: "client";
167
167
  project: "project";
168
168
  agent: "agent";
169
- client: "client";
169
+ user: "user";
170
170
  workspace: "workspace";
171
171
  api_key: "api_key";
172
172
  }>;
@@ -188,8 +188,8 @@ export declare const spendBreakdownEntrySchema: z.ZodObject<{
188
188
  amountUsd: z.ZodNumber;
189
189
  recordCount: z.ZodNumber;
190
190
  confidence: z.ZodEnum<{
191
- estimated: "estimated";
192
191
  verified: "verified";
192
+ estimated: "estimated";
193
193
  detected_unverified: "detected_unverified";
194
194
  missing: "missing";
195
195
  }>;
@@ -206,8 +206,8 @@ export declare const spendAnomalySchema: z.ZodObject<{
206
206
  currentAmountUsd: z.ZodNumber;
207
207
  multiplier: z.ZodNumber;
208
208
  confidence: z.ZodEnum<{
209
- estimated: "estimated";
210
209
  verified: "verified";
210
+ estimated: "estimated";
211
211
  detected_unverified: "detected_unverified";
212
212
  missing: "missing";
213
213
  }>;
@@ -223,8 +223,8 @@ export declare const workflowWatchEntrySchema: z.ZodObject<{
223
223
  shareOfSpend: z.ZodNumber;
224
224
  recordCount: z.ZodNumber;
225
225
  confidence: z.ZodEnum<{
226
- estimated: "estimated";
227
226
  verified: "verified";
227
+ estimated: "estimated";
228
228
  detected_unverified: "detected_unverified";
229
229
  missing: "missing";
230
230
  }>;
@@ -248,8 +248,8 @@ export declare const recommendationSchema: z.ZodObject<{
248
248
  }>;
249
249
  estimatedImpactUsd: z.ZodNumber;
250
250
  confidence: z.ZodEnum<{
251
- estimated: "estimated";
252
251
  verified: "verified";
252
+ estimated: "estimated";
253
253
  detected_unverified: "detected_unverified";
254
254
  missing: "missing";
255
255
  }>;
@@ -293,8 +293,8 @@ export declare const spendInsightSchema: z.ZodObject<{
293
293
  affectedModels: z.ZodArray<z.ZodString>;
294
294
  estimatedImpactUsd: z.ZodNumber;
295
295
  confidence: z.ZodEnum<{
296
- estimated: "estimated";
297
296
  verified: "verified";
297
+ estimated: "estimated";
298
298
  detected_unverified: "detected_unverified";
299
299
  missing: "missing";
300
300
  }>;
@@ -306,14 +306,14 @@ export declare const spendSummarySchema: z.ZodObject<{
306
306
  totalUsd: z.ZodNumber;
307
307
  recordCount: z.ZodNumber;
308
308
  confidence: z.ZodEnum<{
309
- estimated: "estimated";
310
309
  verified: "verified";
310
+ estimated: "estimated";
311
311
  detected_unverified: "detected_unverified";
312
312
  missing: "missing";
313
313
  }>;
314
314
  confidenceBreakdown: z.ZodRecord<z.ZodEnum<{
315
- estimated: "estimated";
316
315
  verified: "verified";
316
+ estimated: "estimated";
317
317
  detected_unverified: "detected_unverified";
318
318
  missing: "missing";
319
319
  }>, z.ZodNumber>;
@@ -322,8 +322,8 @@ export declare const spendSummarySchema: z.ZodObject<{
322
322
  amountUsd: z.ZodNumber;
323
323
  recordCount: z.ZodNumber;
324
324
  confidence: z.ZodEnum<{
325
- estimated: "estimated";
326
325
  verified: "verified";
326
+ estimated: "estimated";
327
327
  detected_unverified: "detected_unverified";
328
328
  missing: "missing";
329
329
  }>;
@@ -333,8 +333,8 @@ export declare const spendSummarySchema: z.ZodObject<{
333
333
  amountUsd: z.ZodNumber;
334
334
  recordCount: z.ZodNumber;
335
335
  confidence: z.ZodEnum<{
336
- estimated: "estimated";
337
336
  verified: "verified";
337
+ estimated: "estimated";
338
338
  detected_unverified: "detected_unverified";
339
339
  missing: "missing";
340
340
  }>;
@@ -344,8 +344,8 @@ export declare const spendSummarySchema: z.ZodObject<{
344
344
  amountUsd: z.ZodNumber;
345
345
  recordCount: z.ZodNumber;
346
346
  confidence: z.ZodEnum<{
347
- estimated: "estimated";
348
347
  verified: "verified";
348
+ estimated: "estimated";
349
349
  detected_unverified: "detected_unverified";
350
350
  missing: "missing";
351
351
  }>;
@@ -355,8 +355,8 @@ export declare const spendSummarySchema: z.ZodObject<{
355
355
  amountUsd: z.ZodNumber;
356
356
  recordCount: z.ZodNumber;
357
357
  confidence: z.ZodEnum<{
358
- estimated: "estimated";
359
358
  verified: "verified";
359
+ estimated: "estimated";
360
360
  detected_unverified: "detected_unverified";
361
361
  missing: "missing";
362
362
  }>;
@@ -366,8 +366,8 @@ export declare const spendSummarySchema: z.ZodObject<{
366
366
  amountUsd: z.ZodNumber;
367
367
  recordCount: z.ZodNumber;
368
368
  confidence: z.ZodEnum<{
369
- estimated: "estimated";
370
369
  verified: "verified";
370
+ estimated: "estimated";
371
371
  detected_unverified: "detected_unverified";
372
372
  missing: "missing";
373
373
  }>;
@@ -377,8 +377,8 @@ export declare const spendSummarySchema: z.ZodObject<{
377
377
  amountUsd: z.ZodNumber;
378
378
  recordCount: z.ZodNumber;
379
379
  confidence: z.ZodEnum<{
380
- estimated: "estimated";
381
380
  verified: "verified";
381
+ estimated: "estimated";
382
382
  detected_unverified: "detected_unverified";
383
383
  missing: "missing";
384
384
  }>;
@@ -388,8 +388,8 @@ export declare const spendSummarySchema: z.ZodObject<{
388
388
  amountUsd: z.ZodNumber;
389
389
  recordCount: z.ZodNumber;
390
390
  confidence: z.ZodEnum<{
391
- estimated: "estimated";
392
391
  verified: "verified";
392
+ estimated: "estimated";
393
393
  detected_unverified: "detected_unverified";
394
394
  missing: "missing";
395
395
  }>;
@@ -399,8 +399,8 @@ export declare const spendSummarySchema: z.ZodObject<{
399
399
  amountUsd: z.ZodNumber;
400
400
  recordCount: z.ZodNumber;
401
401
  confidence: z.ZodEnum<{
402
- estimated: "estimated";
403
402
  verified: "verified";
403
+ estimated: "estimated";
404
404
  detected_unverified: "detected_unverified";
405
405
  missing: "missing";
406
406
  }>;
@@ -415,8 +415,8 @@ export declare const spendSummarySchema: z.ZodObject<{
415
415
  shareOfSpend: z.ZodNumber;
416
416
  recordCount: z.ZodNumber;
417
417
  confidence: z.ZodEnum<{
418
- estimated: "estimated";
419
418
  verified: "verified";
419
+ estimated: "estimated";
420
420
  detected_unverified: "detected_unverified";
421
421
  missing: "missing";
422
422
  }>;
@@ -437,8 +437,8 @@ export declare const spendSummarySchema: z.ZodObject<{
437
437
  currentAmountUsd: z.ZodNumber;
438
438
  multiplier: z.ZodNumber;
439
439
  confidence: z.ZodEnum<{
440
- estimated: "estimated";
441
440
  verified: "verified";
441
+ estimated: "estimated";
442
442
  detected_unverified: "detected_unverified";
443
443
  missing: "missing";
444
444
  }>;
@@ -456,8 +456,8 @@ export declare const spendSummarySchema: z.ZodObject<{
456
456
  }>;
457
457
  estimatedImpactUsd: z.ZodNumber;
458
458
  confidence: z.ZodEnum<{
459
- estimated: "estimated";
460
459
  verified: "verified";
460
+ estimated: "estimated";
461
461
  detected_unverified: "detected_unverified";
462
462
  missing: "missing";
463
463
  }>;
@@ -494,8 +494,8 @@ export declare const spendSummarySchema: z.ZodObject<{
494
494
  affectedModels: z.ZodArray<z.ZodString>;
495
495
  estimatedImpactUsd: z.ZodNumber;
496
496
  confidence: z.ZodEnum<{
497
- estimated: "estimated";
498
497
  verified: "verified";
498
+ estimated: "estimated";
499
499
  detected_unverified: "detected_unverified";
500
500
  missing: "missing";
501
501
  }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-finops/core",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",