@agent-finops/core 0.7.3 → 0.8.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/README.md +35 -2
- package/dist/activitySnapshot.d.ts +8 -8
- package/dist/activitySnapshot.js +20 -9
- package/dist/cutList.js +9 -1
- package/dist/glance.js +3 -1
- package/dist/localAgentFormats/gemini.d.ts +55 -0
- package/dist/localAgentFormats/gemini.js +443 -0
- package/dist/localAgentFormats/registry.d.ts +2 -1
- package/dist/localAgentFormats/registry.js +125 -9
- package/dist/localAgentFormats/runtimeRegistry.js +25 -2
- package/dist/localAgentFormats/types.d.ts +13 -4
- package/dist/localAgentLogs.d.ts +25 -4
- package/dist/localAgentLogs.js +215 -17
- package/dist/modelPricing.d.ts +33 -1
- package/dist/modelPricing.js +117 -13
- package/dist/providerConnectors.d.ts +16 -0
- package/dist/providerConnectors.js +304 -33
- package/dist/providerContractStates.generated.d.ts +8 -0
- package/dist/providerContractStates.generated.js +9 -0
- package/dist/schema.d.ts +18 -0
- package/dist/schema.js +26 -0
- package/dist/sourceStatus.d.ts +23 -1
- package/dist/sourceStatus.js +104 -9
- package/dist/stateTrust.js +2 -2
- package/package.json +8 -1
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { createProviderConnectorStub, slugifySourceId } from "./sourceRegistry.js";
|
|
2
2
|
import { redactSecrets } from "./discovery.js";
|
|
3
|
+
/**
|
|
4
|
+
* Trusted connector failure metadata. Provider prose remains untrusted and is
|
|
5
|
+
* used only as a sanitized human-readable message; callers classify failures
|
|
6
|
+
* from this product-authored code and the observed HTTP status instead.
|
|
7
|
+
*/
|
|
8
|
+
export class ProviderConnectorError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
status;
|
|
11
|
+
constructor(message, options) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "ProviderConnectorError";
|
|
14
|
+
this.code = options.code;
|
|
15
|
+
this.status = options.status;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function isProviderAuthenticationError(error) {
|
|
19
|
+
return error instanceof ProviderConnectorError &&
|
|
20
|
+
error.code === "authentication_error";
|
|
21
|
+
}
|
|
3
22
|
export function normalizeOpenAiCostResponse(response, options) {
|
|
4
23
|
const data = isObject(response) && Array.isArray(response.data) ? response.data : [];
|
|
5
24
|
const records = [];
|
|
@@ -59,29 +78,115 @@ export function normalizeOpenAiCostResponse(response, options) {
|
|
|
59
78
|
export function normalizeOpenAiUsageResponse(response, options) {
|
|
60
79
|
const data = isObject(response) && Array.isArray(response.data) ? response.data : [];
|
|
61
80
|
const records = [];
|
|
62
|
-
for (const
|
|
63
|
-
|
|
81
|
+
for (const bucketValue of data) {
|
|
82
|
+
if (!isRecord(bucketValue) || !Array.isArray(bucketValue.results))
|
|
83
|
+
continue;
|
|
84
|
+
const startTime = validEpochSeconds(bucketValue.start_time);
|
|
85
|
+
if (typeof startTime !== "number")
|
|
86
|
+
continue;
|
|
64
87
|
const timestamp = new Date(startTime * 1000).toISOString();
|
|
65
|
-
for (const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
88
|
+
for (const resultValue of bucketValue.results) {
|
|
89
|
+
if (!isRecord(resultValue))
|
|
90
|
+
continue;
|
|
91
|
+
const result = resultValue;
|
|
92
|
+
const projectId = stringValue(result.project_id);
|
|
93
|
+
const userId = stringValue(result.user_id);
|
|
94
|
+
const apiKeyId = stringValue(result.api_key_id);
|
|
95
|
+
const model = stringValue(result.model) ?? "openai-usage";
|
|
96
|
+
const serviceTier = stringValue(result.service_tier);
|
|
97
|
+
const batch = typeof result.batch === "boolean" ? result.batch : undefined;
|
|
98
|
+
// OpenAI's organization completions Usage API defines these as
|
|
99
|
+
// inclusive totals across text, audio, image, cache reads, and cache
|
|
100
|
+
// writes. The modality/cache fields below are subsets for provenance;
|
|
101
|
+
// adding them again double-counts usage.
|
|
102
|
+
// https://developers.openai.com/cookbook/examples/completions_usage_api
|
|
103
|
+
const inputTokens = nonNegativeIntegerValue(result.input_tokens);
|
|
104
|
+
const outputTokens = nonNegativeIntegerValue(result.output_tokens);
|
|
105
|
+
// A request count does not prove a zero-token result. Preserve explicit
|
|
106
|
+
// 0/0 totals, but fail closed when either canonical total is absent or
|
|
107
|
+
// invalid so missing provider evidence never becomes a verified zero.
|
|
108
|
+
if (typeof inputTokens !== "number" || typeof outputTokens !== "number")
|
|
109
|
+
continue;
|
|
75
110
|
const requestCount = nonNegativeIntegerValue(result.num_model_requests);
|
|
76
|
-
if (inputTokens + outputTokens
|
|
111
|
+
if (inputTokens + outputTokens === 0 && !(typeof requestCount === "number" && requestCount > 0))
|
|
77
112
|
continue;
|
|
113
|
+
const inputComponent = (value) => boundedTokenComponent(value, inputTokens);
|
|
114
|
+
const outputComponent = (value) => boundedTokenComponent(value, outputTokens);
|
|
115
|
+
const cacheReadTokens = inputComponent(result.input_cached_tokens);
|
|
116
|
+
const cachedComponent = (value) => {
|
|
117
|
+
const component = inputComponent(value);
|
|
118
|
+
return component !== undefined && (cacheReadTokens === undefined || component <= cacheReadTokens)
|
|
119
|
+
? component
|
|
120
|
+
: undefined;
|
|
121
|
+
};
|
|
122
|
+
const inputAccountingValid = componentFamilyWithinParent([
|
|
123
|
+
result.input_uncached_tokens,
|
|
124
|
+
result.input_cache_write_tokens,
|
|
125
|
+
result.input_cached_tokens
|
|
126
|
+
], inputTokens);
|
|
127
|
+
const inputModalitiesValid = componentFamilyWithinParent([
|
|
128
|
+
result.input_text_tokens,
|
|
129
|
+
result.input_image_tokens,
|
|
130
|
+
result.input_audio_tokens
|
|
131
|
+
], inputTokens);
|
|
132
|
+
const cachedModalitiesPresent = [
|
|
133
|
+
result.input_cached_text_tokens,
|
|
134
|
+
result.input_cached_image_tokens,
|
|
135
|
+
result.input_cached_audio_tokens
|
|
136
|
+
].some((value) => value !== undefined);
|
|
137
|
+
const cachedModalitiesValid = !cachedModalitiesPresent || (cacheReadTokens !== undefined && componentFamilyWithinParent([
|
|
138
|
+
result.input_cached_text_tokens,
|
|
139
|
+
result.input_cached_image_tokens,
|
|
140
|
+
result.input_cached_audio_tokens
|
|
141
|
+
], cacheReadTokens));
|
|
142
|
+
const outputModalitiesValid = componentFamilyWithinParent([
|
|
143
|
+
result.output_text_tokens,
|
|
144
|
+
result.output_image_tokens,
|
|
145
|
+
result.output_audio_tokens
|
|
146
|
+
], outputTokens);
|
|
78
147
|
records.push({
|
|
79
|
-
id: slugifySourceId(["openai-usage", String(startTime), projectId, userId, apiKeyId, model].filter(Boolean).join("-")),
|
|
148
|
+
id: slugifySourceId(["openai-usage", String(startTime), projectId, userId, apiKeyId, model, serviceTier, batch === undefined ? undefined : `batch-${batch}`].filter(Boolean).join("-")),
|
|
80
149
|
timestamp,
|
|
81
150
|
source: { id: options.sourceId, name: "OpenAI organization usage API", provider: "openai", confidence: "verified", observedFrom: options.observedFrom },
|
|
82
151
|
model,
|
|
83
|
-
inputTokens
|
|
84
|
-
outputTokens
|
|
152
|
+
inputTokens,
|
|
153
|
+
outputTokens,
|
|
154
|
+
...(inputAccountingValid && inputComponent(result.input_uncached_tokens) !== undefined
|
|
155
|
+
? { inputUncachedTokens: inputComponent(result.input_uncached_tokens) }
|
|
156
|
+
: {}),
|
|
157
|
+
...(inputAccountingValid && inputComponent(result.input_cache_write_tokens) !== undefined
|
|
158
|
+
? { inputCacheWriteTokens: inputComponent(result.input_cache_write_tokens) }
|
|
159
|
+
: {}),
|
|
160
|
+
...(inputAccountingValid && cacheReadTokens !== undefined
|
|
161
|
+
? { cacheReadTokens }
|
|
162
|
+
: {}),
|
|
163
|
+
...(inputModalitiesValid && inputComponent(result.input_text_tokens) !== undefined
|
|
164
|
+
? { inputTextTokens: inputComponent(result.input_text_tokens) }
|
|
165
|
+
: {}),
|
|
166
|
+
...(inputModalitiesValid && inputComponent(result.input_image_tokens) !== undefined
|
|
167
|
+
? { inputImageTokens: inputComponent(result.input_image_tokens) }
|
|
168
|
+
: {}),
|
|
169
|
+
...(inputModalitiesValid && inputComponent(result.input_audio_tokens) !== undefined
|
|
170
|
+
? { inputAudioTokens: inputComponent(result.input_audio_tokens) }
|
|
171
|
+
: {}),
|
|
172
|
+
...(cachedModalitiesValid && cachedComponent(result.input_cached_text_tokens) !== undefined
|
|
173
|
+
? { inputCachedTextTokens: cachedComponent(result.input_cached_text_tokens) }
|
|
174
|
+
: {}),
|
|
175
|
+
...(cachedModalitiesValid && cachedComponent(result.input_cached_image_tokens) !== undefined
|
|
176
|
+
? { inputCachedImageTokens: cachedComponent(result.input_cached_image_tokens) }
|
|
177
|
+
: {}),
|
|
178
|
+
...(cachedModalitiesValid && cachedComponent(result.input_cached_audio_tokens) !== undefined
|
|
179
|
+
? { inputCachedAudioTokens: cachedComponent(result.input_cached_audio_tokens) }
|
|
180
|
+
: {}),
|
|
181
|
+
...(outputModalitiesValid && outputComponent(result.output_text_tokens) !== undefined
|
|
182
|
+
? { outputTextTokens: outputComponent(result.output_text_tokens) }
|
|
183
|
+
: {}),
|
|
184
|
+
...(outputModalitiesValid && outputComponent(result.output_image_tokens) !== undefined
|
|
185
|
+
? { outputImageTokens: outputComponent(result.output_image_tokens) }
|
|
186
|
+
: {}),
|
|
187
|
+
...(outputModalitiesValid && outputComponent(result.output_audio_tokens) !== undefined
|
|
188
|
+
? { outputAudioTokens: outputComponent(result.output_audio_tokens) }
|
|
189
|
+
: {}),
|
|
85
190
|
amountUsd: null,
|
|
86
191
|
costConfidence: "missing",
|
|
87
192
|
projectId,
|
|
@@ -90,12 +195,24 @@ export function normalizeOpenAiUsageResponse(response, options) {
|
|
|
90
195
|
providerCostType: "openai_usage_evidence",
|
|
91
196
|
usageGranularity: "usage_bucket",
|
|
92
197
|
quantity: requestCount,
|
|
198
|
+
...(serviceTier ? { serviceTier } : {}),
|
|
199
|
+
...(batch !== undefined ? { batch } : {}),
|
|
93
200
|
operation: "OpenAI completions usage evidence"
|
|
94
201
|
});
|
|
95
202
|
}
|
|
96
203
|
}
|
|
97
204
|
return records;
|
|
98
205
|
}
|
|
206
|
+
function boundedTokenComponent(value, inclusiveTotal) {
|
|
207
|
+
const component = nonNegativeIntegerValue(value);
|
|
208
|
+
return typeof component === "number" && component <= inclusiveTotal ? component : undefined;
|
|
209
|
+
}
|
|
210
|
+
function componentFamilyWithinParent(values, parent) {
|
|
211
|
+
const components = values
|
|
212
|
+
.map(nonNegativeIntegerValue)
|
|
213
|
+
.filter((value) => typeof value === "number");
|
|
214
|
+
return components.reduce((sum, value) => sum + value, 0) <= parent;
|
|
215
|
+
}
|
|
99
216
|
export function normalizeAnthropicClaudeCodeUsageResponse(response, options) {
|
|
100
217
|
const rows = extractArray(response, "data");
|
|
101
218
|
const records = [];
|
|
@@ -377,7 +494,10 @@ function redactResolvedCredentialError(error, credentialVariants) {
|
|
|
377
494
|
// provider can splice ANSI bytes through an opaque credential so the first
|
|
378
495
|
// literal replacement misses it and control stripping reconstructs it.
|
|
379
496
|
const safeMessage = exactRedactCredentialValues(sanitizeProviderMessage(withoutResolvedCredential), credentialVariants).trim();
|
|
380
|
-
|
|
497
|
+
const message = safeMessage || "Provider connector request failed without a safe error message.";
|
|
498
|
+
return error instanceof ProviderConnectorError
|
|
499
|
+
? new ProviderConnectorError(message, { code: error.code, status: error.status })
|
|
500
|
+
: new Error(message);
|
|
381
501
|
}
|
|
382
502
|
function redactResolvedCredentialValue(value, credentialVariants) {
|
|
383
503
|
if (typeof value === "string") {
|
|
@@ -400,12 +520,15 @@ async function fetchOpenAi(input, token, fetcher, sourceId) {
|
|
|
400
520
|
};
|
|
401
521
|
const costFetch = await fetchPaginatedJson(fetcher, buildOpenAiCostsUrl(input.startTime, input.endTime), request, "openai", "OpenAI costs API");
|
|
402
522
|
markMalformedCostRows(costFetch, "openai", "OpenAI costs API");
|
|
523
|
+
enforceOpenAiRequestedWindow(costFetch, input.startTime, input.endTime, "OpenAI costs API");
|
|
403
524
|
const usageFetch = await fetchPaginatedJson(fetcher, buildOpenAiUsageUrl(input.startTime, input.endTime), request, "openai", "OpenAI usage API");
|
|
404
525
|
markMalformedUsageRows(usageFetch, "openai", "OpenAI usage API");
|
|
405
|
-
|
|
526
|
+
enforceOpenAiRequestedWindow(usageFetch, input.startTime, input.endTime, "OpenAI usage API");
|
|
527
|
+
const normalizedRecords = [
|
|
406
528
|
...costFetch.pages.flatMap((page) => normalizeOpenAiCostResponse(page, { sourceId, observedFrom: "OpenAI organization costs API" })),
|
|
407
529
|
...usageFetch.pages.flatMap((page) => normalizeOpenAiUsageResponse(page, { sourceId, observedFrom: "OpenAI organization usage API" }))
|
|
408
530
|
];
|
|
531
|
+
const records = dedupeProviderRecords(normalizedRecords, [costFetch, usageFetch]);
|
|
409
532
|
return providerResult("openai", sourceId, input.authReference, records, qaSummary("openai", [costFetch, usageFetch]), requestedCoverageInterval(input));
|
|
410
533
|
}
|
|
411
534
|
async function fetchAnthropic(input, token, fetcher, sourceId) {
|
|
@@ -587,7 +710,7 @@ async function fetchTextOrThrow(fetcher, url, request, provider, label) {
|
|
|
587
710
|
return response.text();
|
|
588
711
|
}
|
|
589
712
|
const payload = await response.json().catch(() => undefined);
|
|
590
|
-
lastError =
|
|
713
|
+
lastError = providerRequestError(provider, label, response, payload);
|
|
591
714
|
const retryable = response.status === 429 || response.status >= 500;
|
|
592
715
|
if (!retryable || attempt === maxFetchRetries)
|
|
593
716
|
break;
|
|
@@ -664,7 +787,20 @@ async function fetchPaginatedJson(fetcher, initialUrl, request, provider, label)
|
|
|
664
787
|
let stoppedBecause = "complete";
|
|
665
788
|
let note;
|
|
666
789
|
const maxPages = 50;
|
|
790
|
+
const seenUrls = new Set();
|
|
667
791
|
for (let pageCount = 0; nextUrl && pageCount < maxPages; pageCount += 1) {
|
|
792
|
+
if (seenUrls.has(nextUrl)) {
|
|
793
|
+
stoppedBecause = "missing_cursor";
|
|
794
|
+
note = `Stopped after ${pages.length} page(s): provider repeated a pagination URL.`;
|
|
795
|
+
responseDrift.push({
|
|
796
|
+
label,
|
|
797
|
+
field: "next_page",
|
|
798
|
+
issue: "provider repeated a pagination cursor; duplicate pages were not fetched"
|
|
799
|
+
});
|
|
800
|
+
nextUrl = undefined;
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
seenUrls.add(nextUrl);
|
|
668
804
|
let response;
|
|
669
805
|
try {
|
|
670
806
|
response = await fetchJsonOrThrow(fetcher, nextUrl, request, provider, label);
|
|
@@ -721,6 +857,64 @@ async function fetchPaginatedJson(fetcher, initialUrl, request, provider, label)
|
|
|
721
857
|
responseDrift
|
|
722
858
|
};
|
|
723
859
|
}
|
|
860
|
+
function enforceOpenAiRequestedWindow(fetchResult, requestedStart, requestedEnd, label) {
|
|
861
|
+
let excluded = 0;
|
|
862
|
+
const nowEpochSeconds = Math.floor(Date.now() / 1000);
|
|
863
|
+
fetchResult.pages = fetchResult.pages.map((page) => {
|
|
864
|
+
if (!isRecord(page) || !Array.isArray(page.data))
|
|
865
|
+
return page;
|
|
866
|
+
const data = page.data.filter((bucket) => {
|
|
867
|
+
if (!isRecord(bucket))
|
|
868
|
+
return true;
|
|
869
|
+
const start = validEpochSeconds(bucket.start_time);
|
|
870
|
+
const hasEnd = bucket.end_time !== undefined;
|
|
871
|
+
const end = hasEnd ? validEpochSeconds(bucket.end_time) : undefined;
|
|
872
|
+
const invalidEnd = hasEnd && (typeof end !== "number" || typeof start !== "number" || end <= start);
|
|
873
|
+
const missingRequiredEnd = typeof requestedEnd === "number" && typeof end !== "number";
|
|
874
|
+
const outsideStart = typeof start !== "number" || start < requestedStart ||
|
|
875
|
+
(typeof requestedEnd === "number" && start >= requestedEnd);
|
|
876
|
+
const outsideEnd = typeof requestedEnd === "number" && typeof end === "number" && end > requestedEnd;
|
|
877
|
+
const futureOpenEndedStart = requestedEnd === undefined && typeof start === "number" && start > nowEpochSeconds;
|
|
878
|
+
if (!invalidEnd && !missingRequiredEnd && !outsideStart && !outsideEnd && !futureOpenEndedStart)
|
|
879
|
+
return true;
|
|
880
|
+
excluded += 1;
|
|
881
|
+
fetchResult.responseDrift.push({
|
|
882
|
+
label,
|
|
883
|
+
field: "data[].start_time/end_time",
|
|
884
|
+
issue: "bucket boundary was missing, invalid, future-starting, or outside the requested interval; the bucket was excluded"
|
|
885
|
+
});
|
|
886
|
+
return false;
|
|
887
|
+
});
|
|
888
|
+
return { ...page, data };
|
|
889
|
+
});
|
|
890
|
+
if (excluded > 0)
|
|
891
|
+
fetchResult.coverageIncomplete = true;
|
|
892
|
+
}
|
|
893
|
+
function dedupeProviderRecords(records, fetches) {
|
|
894
|
+
const byId = new Map();
|
|
895
|
+
const conflicted = new Set();
|
|
896
|
+
for (const record of records) {
|
|
897
|
+
const existing = byId.get(record.id);
|
|
898
|
+
if (!existing && !conflicted.has(record.id)) {
|
|
899
|
+
byId.set(record.id, record);
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const target = record.providerCostType === "openai_cost" ? fetches[0] : fetches[1];
|
|
903
|
+
target.coverageIncomplete = true;
|
|
904
|
+
target.responseDrift.push({
|
|
905
|
+
label: target.pagination.label,
|
|
906
|
+
field: "normalized records[].id",
|
|
907
|
+
issue: existing && JSON.stringify(existing) === JSON.stringify(record)
|
|
908
|
+
? "duplicate provider record was excluded"
|
|
909
|
+
: "conflicting provider records shared one stable identity; all conflicting rows were excluded"
|
|
910
|
+
});
|
|
911
|
+
if (existing && JSON.stringify(existing) !== JSON.stringify(record)) {
|
|
912
|
+
byId.delete(record.id);
|
|
913
|
+
conflicted.add(record.id);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return Array.from(byId.values());
|
|
917
|
+
}
|
|
724
918
|
function validatePaginationUrl(initialUrl, candidate) {
|
|
725
919
|
try {
|
|
726
920
|
const initial = new URL(initialUrl);
|
|
@@ -793,7 +987,7 @@ async function fetchJsonOrThrow(fetcher, url, request, provider, label) {
|
|
|
793
987
|
if (response.ok) {
|
|
794
988
|
return { payload, rateLimit: rateLimitFromHeaders(label, response.headers), headers: response.headers };
|
|
795
989
|
}
|
|
796
|
-
lastError =
|
|
990
|
+
lastError = providerRequestError(provider, label, response, payload);
|
|
797
991
|
// 429 and 5xx are transient: honor retry-after when present, otherwise
|
|
798
992
|
// back off briefly and try again. 4xx auth/scope errors fail immediately.
|
|
799
993
|
const retryable = response.status === 429 || response.status >= 500;
|
|
@@ -978,6 +1172,9 @@ function markMalformedUsageRows(fetchResult, provider, label) {
|
|
|
978
1172
|
report(bucketPath, "usage response contained a malformed bucket or omitted its results array");
|
|
979
1173
|
continue;
|
|
980
1174
|
}
|
|
1175
|
+
if (typeof validEpochSeconds(bucketValue.start_time) !== "number") {
|
|
1176
|
+
report(`${bucketPath}.start_time`, "usage bucket start_time must be a non-negative whole-second timestamp; the bucket was excluded");
|
|
1177
|
+
}
|
|
981
1178
|
for (const [resultIndex, resultValue] of bucketValue.results.entries()) {
|
|
982
1179
|
const resultPath = `${bucketPath}.results[${resultIndex}]`;
|
|
983
1180
|
if (!isRecord(resultValue)) {
|
|
@@ -1003,6 +1200,67 @@ function markMalformedUsageRows(fetchResult, provider, label) {
|
|
|
1003
1200
|
checkInteger(resultValue[field], `${resultPath}.${field}`, "token count");
|
|
1004
1201
|
}
|
|
1005
1202
|
checkInteger(resultValue.num_model_requests, `${resultPath}.num_model_requests`, "quantity");
|
|
1203
|
+
const inputTotal = nonNegativeIntegerValue(resultValue.input_tokens);
|
|
1204
|
+
const outputTotal = nonNegativeIntegerValue(resultValue.output_tokens);
|
|
1205
|
+
const cachedTotal = nonNegativeIntegerValue(resultValue.input_cached_tokens);
|
|
1206
|
+
if (typeof inputTotal !== "number") {
|
|
1207
|
+
report(`${resultPath}.input_tokens`, "canonical input_tokens total is required; the usage row was excluded");
|
|
1208
|
+
}
|
|
1209
|
+
if (typeof outputTotal !== "number") {
|
|
1210
|
+
report(`${resultPath}.output_tokens`, "canonical output_tokens total is required; the usage row was excluded");
|
|
1211
|
+
}
|
|
1212
|
+
for (const field of [
|
|
1213
|
+
"input_uncached_tokens",
|
|
1214
|
+
"input_cache_write_tokens",
|
|
1215
|
+
"input_cached_tokens",
|
|
1216
|
+
"input_text_tokens",
|
|
1217
|
+
"input_image_tokens",
|
|
1218
|
+
"input_audio_tokens",
|
|
1219
|
+
"input_cached_text_tokens",
|
|
1220
|
+
"input_cached_image_tokens",
|
|
1221
|
+
"input_cached_audio_tokens"
|
|
1222
|
+
]) {
|
|
1223
|
+
const component = nonNegativeIntegerValue(resultValue[field]);
|
|
1224
|
+
if (typeof component === "number" && typeof inputTotal === "number" && component > inputTotal) {
|
|
1225
|
+
report(`${resultPath}.${field}`, "input component exceeds the inclusive input_tokens total; the component was excluded");
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
for (const field of ["output_text_tokens", "output_image_tokens", "output_audio_tokens"]) {
|
|
1229
|
+
const component = nonNegativeIntegerValue(resultValue[field]);
|
|
1230
|
+
if (typeof component === "number" && typeof outputTotal === "number" && component > outputTotal) {
|
|
1231
|
+
report(`${resultPath}.${field}`, "output component exceeds the inclusive output_tokens total; the component was excluded");
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
for (const field of [
|
|
1235
|
+
"input_cached_text_tokens",
|
|
1236
|
+
"input_cached_image_tokens",
|
|
1237
|
+
"input_cached_audio_tokens"
|
|
1238
|
+
]) {
|
|
1239
|
+
const component = nonNegativeIntegerValue(resultValue[field]);
|
|
1240
|
+
if (typeof component === "number" && typeof cachedTotal === "number" && component > cachedTotal) {
|
|
1241
|
+
report(`${resultPath}.${field}`, "cached component exceeds input_cached_tokens; the component was excluded");
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (typeof cachedTotal !== "number" && [
|
|
1245
|
+
resultValue.input_cached_text_tokens,
|
|
1246
|
+
resultValue.input_cached_image_tokens,
|
|
1247
|
+
resultValue.input_cached_audio_tokens
|
|
1248
|
+
].some((value) => typeof nonNegativeIntegerValue(value) === "number")) {
|
|
1249
|
+
report(`${resultPath}.input_cached_*_tokens`, "cached modality components require input_cached_tokens; the component family was excluded");
|
|
1250
|
+
}
|
|
1251
|
+
const reportFamilyOverflow = (fields, parent, parentField) => {
|
|
1252
|
+
if (typeof parent !== "number")
|
|
1253
|
+
return;
|
|
1254
|
+
const values = fields.map((field) => nonNegativeIntegerValue(resultValue[field]));
|
|
1255
|
+
const present = values.filter((value) => typeof value === "number");
|
|
1256
|
+
if (present.reduce((sum, value) => sum + value, 0) > parent) {
|
|
1257
|
+
report(`${resultPath}.${fields.join("+")}`, `component family exceeds ${parentField}; the contradictory component family was excluded`);
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
reportFamilyOverflow(["input_uncached_tokens", "input_cache_write_tokens", "input_cached_tokens"], inputTotal, "input_tokens");
|
|
1261
|
+
reportFamilyOverflow(["input_text_tokens", "input_image_tokens", "input_audio_tokens"], inputTotal, "input_tokens");
|
|
1262
|
+
reportFamilyOverflow(["input_cached_text_tokens", "input_cached_image_tokens", "input_cached_audio_tokens"], cachedTotal, "input_cached_tokens");
|
|
1263
|
+
reportFamilyOverflow(["output_text_tokens", "output_image_tokens", "output_audio_tokens"], outputTotal, "output_tokens");
|
|
1006
1264
|
}
|
|
1007
1265
|
}
|
|
1008
1266
|
continue;
|
|
@@ -1195,6 +1453,13 @@ function providerPermissionPrompt(provider, label, response, payload) {
|
|
|
1195
1453
|
}
|
|
1196
1454
|
return `${label} request failed with ${status}. ${rawMessage}`.trim();
|
|
1197
1455
|
}
|
|
1456
|
+
function providerRequestError(provider, label, response, payload) {
|
|
1457
|
+
const authenticationFailure = response.status === 401 || response.status === 403;
|
|
1458
|
+
return new ProviderConnectorError(providerPermissionPrompt(provider, label, response, payload), {
|
|
1459
|
+
code: authenticationFailure ? "authentication_error" : "provider_request_error",
|
|
1460
|
+
status: response.status
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1198
1463
|
function extractProviderMessage(payload) {
|
|
1199
1464
|
if (!isRecord(payload))
|
|
1200
1465
|
return "";
|
|
@@ -1238,6 +1503,17 @@ export function summarizeProviderFinancials(records) {
|
|
|
1238
1503
|
}
|
|
1239
1504
|
return { providerReportedBilledUsd, apiEquivalentEstimatedUsd, providerEstimatedUsd, headlineUsd: null, headlineBasis: "unavailable" };
|
|
1240
1505
|
}
|
|
1506
|
+
export function providerFinancialCompleteness(records, coverage) {
|
|
1507
|
+
const financials = summarizeProviderFinancials(records);
|
|
1508
|
+
const headlineConfidence = financials.headlineBasis === "provider_reported_billed_cost"
|
|
1509
|
+
? "verified"
|
|
1510
|
+
: financials.headlineBasis === "unavailable"
|
|
1511
|
+
? "missing"
|
|
1512
|
+
: "estimated";
|
|
1513
|
+
return coverage === "partial" && headlineConfidence !== "missing"
|
|
1514
|
+
? "detected_unverified"
|
|
1515
|
+
: headlineConfidence;
|
|
1516
|
+
}
|
|
1241
1517
|
/**
|
|
1242
1518
|
* Keep evidence records available to callers, but never add estimates to a
|
|
1243
1519
|
* provider's official billed total. This selection is intended for aggregate
|
|
@@ -1268,14 +1544,7 @@ function providerResult(provider, sourceId, authReference, records, qa, coverage
|
|
|
1268
1544
|
const coverage = resolvedQa.coverage
|
|
1269
1545
|
?? (resolvedQa.pagination.every((pagination) => pagination.stoppedBecause === "complete") ? "complete" : "partial");
|
|
1270
1546
|
const financials = summarizeProviderFinancials(records);
|
|
1271
|
-
const
|
|
1272
|
-
? "verified"
|
|
1273
|
-
: financials.headlineBasis === "unavailable"
|
|
1274
|
-
? "missing"
|
|
1275
|
-
: "estimated";
|
|
1276
|
-
const completeness = coverage === "partial" && headlineConfidence !== "missing"
|
|
1277
|
-
? "detected_unverified"
|
|
1278
|
-
: headlineConfidence;
|
|
1547
|
+
const completeness = providerFinancialCompleteness(records, coverage);
|
|
1279
1548
|
return {
|
|
1280
1549
|
provider,
|
|
1281
1550
|
source: createProviderConnection({ provider, sourceId, authReference, verifiedRecordCount: records.length, totalUsd: financials.headlineUsd, completeness }),
|
|
@@ -1328,7 +1597,7 @@ export function createProviderConnection(input) {
|
|
|
1328
1597
|
validationCoverage: validationCoverageForCompletedProviderSync(input.provider),
|
|
1329
1598
|
financialEvidence,
|
|
1330
1599
|
authReference: input.authReference,
|
|
1331
|
-
fieldsMissing: [],
|
|
1600
|
+
fieldsMissing: financialEvidence === "missing" ? ["provider financial headline"] : [],
|
|
1332
1601
|
scope: `${source.scope} Last successful pull produced ${input.verifiedRecordCount} record(s); financial evidence: ${financialEvidence}; financial headline: ${total}.`
|
|
1333
1602
|
};
|
|
1334
1603
|
}
|
|
@@ -1345,15 +1614,15 @@ function formatProviderUsd(value) {
|
|
|
1345
1614
|
}
|
|
1346
1615
|
export function resolveTokenReference(reference, env = process.env) {
|
|
1347
1616
|
if (!reference.startsWith("env:")) {
|
|
1348
|
-
throw new
|
|
1617
|
+
throw new ProviderConnectorError("Provider auth reference must be a local reference such as env:OPENAI_ADMIN_KEY; raw secrets are not accepted.", { code: "authentication_error" });
|
|
1349
1618
|
}
|
|
1350
1619
|
const envName = reference.slice("env:".length);
|
|
1351
1620
|
if (!/^[A-Z0-9_]+$/.test(envName)) {
|
|
1352
|
-
throw new
|
|
1621
|
+
throw new ProviderConnectorError("Provider auth env reference must use an uppercase environment variable name.", { code: "authentication_error" });
|
|
1353
1622
|
}
|
|
1354
1623
|
const value = env[envName];
|
|
1355
1624
|
if (!value) {
|
|
1356
|
-
throw new
|
|
1625
|
+
throw new ProviderConnectorError(`Provider auth reference ${reference} is not set in the local environment.`, { code: "authentication_error" });
|
|
1357
1626
|
}
|
|
1358
1627
|
return value;
|
|
1359
1628
|
}
|
|
@@ -1378,6 +1647,8 @@ function buildOpenAiUsageUrl(startTime, endTime) {
|
|
|
1378
1647
|
url.searchParams.append("group_by", "user_id");
|
|
1379
1648
|
url.searchParams.append("group_by", "api_key_id");
|
|
1380
1649
|
url.searchParams.append("group_by", "model");
|
|
1650
|
+
url.searchParams.append("group_by", "batch");
|
|
1651
|
+
url.searchParams.append("group_by", "service_tier");
|
|
1381
1652
|
if (endTime !== undefined)
|
|
1382
1653
|
url.searchParams.set("end_time", String(endTime));
|
|
1383
1654
|
return url.toString();
|
|
@@ -1449,7 +1720,7 @@ function stringValue(value) {
|
|
|
1449
1720
|
}
|
|
1450
1721
|
function validEpochSeconds(value) {
|
|
1451
1722
|
const seconds = numberValue(value);
|
|
1452
|
-
return typeof seconds === "number" && seconds >= 0 && Number.isFinite(new Date(seconds * 1000).getTime())
|
|
1723
|
+
return typeof seconds === "number" && Number.isInteger(seconds) && seconds >= 0 && Number.isFinite(new Date(seconds * 1000).getTime())
|
|
1453
1724
|
? seconds
|
|
1454
1725
|
: undefined;
|
|
1455
1726
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const generatedProviderContractStates: {
|
|
2
|
+
readonly anthropic: "current";
|
|
3
|
+
readonly cursor: "current";
|
|
4
|
+
readonly "gemini-cli": "current";
|
|
5
|
+
readonly "github-copilot": "current";
|
|
6
|
+
readonly openai: "current";
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=providerContractStates.generated.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// GENERATED by scripts/generate-provider-contract-docs.mjs. Do not edit by hand.
|
|
2
|
+
export const generatedProviderContractStates = {
|
|
3
|
+
"anthropic": "current",
|
|
4
|
+
"cursor": "current",
|
|
5
|
+
"gemini-cli": "current",
|
|
6
|
+
"github-copilot": "current",
|
|
7
|
+
"openai": "current",
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=providerContractStates.generated.js.map
|
package/dist/schema.d.ts
CHANGED
|
@@ -69,6 +69,24 @@ export declare const usageRecordSchema: z.ZodObject<{
|
|
|
69
69
|
model: z.ZodString;
|
|
70
70
|
inputTokens: z.ZodNumber;
|
|
71
71
|
outputTokens: z.ZodNumber;
|
|
72
|
+
cacheReadTokens: z.ZodOptional<z.ZodNumber>;
|
|
73
|
+
inputUncachedTokens: z.ZodOptional<z.ZodNumber>;
|
|
74
|
+
inputCacheWriteTokens: z.ZodOptional<z.ZodNumber>;
|
|
75
|
+
inputTextTokens: z.ZodOptional<z.ZodNumber>;
|
|
76
|
+
inputImageTokens: z.ZodOptional<z.ZodNumber>;
|
|
77
|
+
inputAudioTokens: z.ZodOptional<z.ZodNumber>;
|
|
78
|
+
inputCachedTextTokens: z.ZodOptional<z.ZodNumber>;
|
|
79
|
+
inputCachedImageTokens: z.ZodOptional<z.ZodNumber>;
|
|
80
|
+
inputCachedAudioTokens: z.ZodOptional<z.ZodNumber>;
|
|
81
|
+
outputTextTokens: z.ZodOptional<z.ZodNumber>;
|
|
82
|
+
outputImageTokens: z.ZodOptional<z.ZodNumber>;
|
|
83
|
+
outputAudioTokens: z.ZodOptional<z.ZodNumber>;
|
|
84
|
+
serviceTier: z.ZodOptional<z.ZodString>;
|
|
85
|
+
batch: z.ZodOptional<z.ZodBoolean>;
|
|
86
|
+
thoughtTokens: z.ZodOptional<z.ZodNumber>;
|
|
87
|
+
toolTokens: z.ZodOptional<z.ZodNumber>;
|
|
88
|
+
reportedTotalTokens: z.ZodOptional<z.ZodNumber>;
|
|
89
|
+
sourceVersions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
72
90
|
amountUsd: z.ZodNullable<z.ZodNumber>;
|
|
73
91
|
costConfidence: z.ZodEnum<{
|
|
74
92
|
verified: "verified";
|
package/dist/schema.js
CHANGED
|
@@ -47,8 +47,34 @@ export const usageRecordSchema = z.object({
|
|
|
47
47
|
timestamp: z.string().datetime({ offset: true }),
|
|
48
48
|
source: spendSourceSchema,
|
|
49
49
|
model: z.string().min(1),
|
|
50
|
+
/** Inclusive input-side total. Gemini includes cached + tool tokens here. */
|
|
50
51
|
inputTokens: z.number().int().nonnegative(),
|
|
52
|
+
/** Inclusive output-side total. Gemini includes thought tokens here. */
|
|
51
53
|
outputTokens: z.number().int().nonnegative(),
|
|
54
|
+
/**
|
|
55
|
+
* Optional component subsets retained as provenance. These fields are
|
|
56
|
+
* already included in inputTokens/outputTokens and must not be added again.
|
|
57
|
+
*/
|
|
58
|
+
cacheReadTokens: z.number().int().nonnegative().optional(),
|
|
59
|
+
inputUncachedTokens: z.number().int().nonnegative().optional(),
|
|
60
|
+
inputCacheWriteTokens: z.number().int().nonnegative().optional(),
|
|
61
|
+
inputTextTokens: z.number().int().nonnegative().optional(),
|
|
62
|
+
inputImageTokens: z.number().int().nonnegative().optional(),
|
|
63
|
+
inputAudioTokens: z.number().int().nonnegative().optional(),
|
|
64
|
+
inputCachedTextTokens: z.number().int().nonnegative().optional(),
|
|
65
|
+
inputCachedImageTokens: z.number().int().nonnegative().optional(),
|
|
66
|
+
inputCachedAudioTokens: z.number().int().nonnegative().optional(),
|
|
67
|
+
outputTextTokens: z.number().int().nonnegative().optional(),
|
|
68
|
+
outputImageTokens: z.number().int().nonnegative().optional(),
|
|
69
|
+
outputAudioTokens: z.number().int().nonnegative().optional(),
|
|
70
|
+
/** Provider grouping dimensions retained so bucket identities stay unique. */
|
|
71
|
+
serviceTier: z.string().min(1).optional(),
|
|
72
|
+
batch: z.boolean().optional(),
|
|
73
|
+
thoughtTokens: z.number().int().nonnegative().optional(),
|
|
74
|
+
toolTokens: z.number().int().nonnegative().optional(),
|
|
75
|
+
reportedTotalTokens: z.number().int().nonnegative().optional(),
|
|
76
|
+
/** Sanitized CLI/source versions observed within this aggregate. */
|
|
77
|
+
sourceVersions: z.array(z.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9.+_-]*$/)).max(8).optional(),
|
|
52
78
|
amountUsd: z.number().nonnegative().nullable(),
|
|
53
79
|
costConfidence: costConfidenceSchema,
|
|
54
80
|
clientId: z.string().min(1).optional(),
|
package/dist/sourceStatus.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CostConfidence, UsageRecord } from "./schema.js";
|
|
2
|
+
import type { SourceRegistry } from "./sourceRegistry.js";
|
|
2
3
|
import type { LocalAgentFormatId } from "./localAgentFormats/types.js";
|
|
3
4
|
/**
|
|
4
5
|
* How thoroughly an ingestion path itself has been exercised.
|
|
@@ -12,6 +13,8 @@ export type SourceValidationCoverage = typeof sourceValidationCoverageValues[num
|
|
|
12
13
|
export type FinancialEvidenceStatus = CostConfidence;
|
|
13
14
|
export declare const sourceFreshnessStatusValues: readonly ["fresh", "stale", "not_checked"];
|
|
14
15
|
export type SourceFreshnessStatus = typeof sourceFreshnessStatusValues[number];
|
|
16
|
+
export declare const providerContractStateValues: readonly ["current", "stale_contract"];
|
|
17
|
+
export type ProviderContractState = typeof providerContractStateValues[number];
|
|
15
18
|
export type SourceStatusId = LocalAgentFormatId | "openai" | "anthropic" | "cursor" | "github-copilot";
|
|
16
19
|
export type SourceStatusDefinition = {
|
|
17
20
|
id: SourceStatusId;
|
|
@@ -19,6 +22,8 @@ export type SourceStatusDefinition = {
|
|
|
19
22
|
validationCoverage: SourceValidationCoverage;
|
|
20
23
|
validationNote: string;
|
|
21
24
|
staleAfterHours: number;
|
|
25
|
+
/** Review state of the provider/parser financial semantics used by this source. */
|
|
26
|
+
contractState?: ProviderContractState;
|
|
22
27
|
};
|
|
23
28
|
export type SourceStatusObservation = {
|
|
24
29
|
id: SourceStatusId;
|
|
@@ -32,6 +37,8 @@ export type SourceStatusObservation = {
|
|
|
32
37
|
lastError?: string;
|
|
33
38
|
/** Runtime failures may override the shipped validation baseline. */
|
|
34
39
|
validationCoverage?: SourceValidationCoverage;
|
|
40
|
+
/** A drift monitor or reviewed contract update may fail this source closed. */
|
|
41
|
+
contractState?: ProviderContractState;
|
|
35
42
|
};
|
|
36
43
|
export type SourceStatusFreshness = {
|
|
37
44
|
status: SourceFreshnessStatus;
|
|
@@ -46,6 +53,7 @@ export type SourceStatus = {
|
|
|
46
53
|
validationNote: string;
|
|
47
54
|
financialEvidence: FinancialEvidenceStatus;
|
|
48
55
|
financialEvidenceNote: string;
|
|
56
|
+
contractState?: ProviderContractState;
|
|
49
57
|
freshness: SourceStatusFreshness;
|
|
50
58
|
lastError?: string;
|
|
51
59
|
};
|
|
@@ -55,12 +63,26 @@ export type SourceStatus = {
|
|
|
55
63
|
* reconciliation.
|
|
56
64
|
*/
|
|
57
65
|
export declare const sourceStatusDefinitions: readonly SourceStatusDefinition[];
|
|
58
|
-
export declare function buildSourceStatuses(observations?: readonly SourceStatusObservation[], now?: Date): SourceStatus[];
|
|
66
|
+
export declare function buildSourceStatuses(observations?: readonly SourceStatusObservation[], now?: Date, definitions?: readonly SourceStatusDefinition[]): SourceStatus[];
|
|
59
67
|
/**
|
|
60
68
|
* Reduce one source's current rows to the evidence label used for its headline.
|
|
61
69
|
* Verified billed cost wins; otherwise estimates win over an unpriced signal.
|
|
62
70
|
*/
|
|
63
71
|
export declare function financialEvidenceForRecords(records: readonly UsageRecord[]): FinancialEvidenceStatus;
|
|
72
|
+
/**
|
|
73
|
+
* Apply the reviewed provider-contract gate before any connected financial
|
|
74
|
+
* calculation. Local transcript evidence is unchanged. When a provider's
|
|
75
|
+
* financial semantics are stale, its priced rows remain present for audit and
|
|
76
|
+
* attribution but cannot carry a dollar amount or proof-level confidence.
|
|
77
|
+
*/
|
|
78
|
+
export declare function applyProviderContractGate(records: readonly UsageRecord[], definitions?: readonly SourceStatusDefinition[]): UsageRecord[];
|
|
79
|
+
/**
|
|
80
|
+
* Project persisted source metadata through the same fail-closed contract
|
|
81
|
+
* gate as records. This is read-time only: the signed local receipt remains
|
|
82
|
+
* byte-exact, while an upgraded release cannot repeat an obsolete verified
|
|
83
|
+
* claim from an older sources.json.
|
|
84
|
+
*/
|
|
85
|
+
export declare function applyProviderContractGateToSourceRegistry(registry: SourceRegistry, definitions?: readonly SourceStatusDefinition[]): SourceRegistry;
|
|
64
86
|
/** Stable, plain-text formatter shared by terminal surfaces. */
|
|
65
87
|
export declare function formatSourceStatuses(statuses: readonly SourceStatus[]): string;
|
|
66
88
|
//# sourceMappingURL=sourceStatus.d.ts.map
|