@agent-finops/core 0.7.0 → 0.7.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.
- package/dist/agentEconomicsReceipt.d.ts +934 -0
- package/dist/agentEconomicsReceipt.js +1005 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1005 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { TextDecoder } from "node:util";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { costConfidenceSchema, usageGranularitySchema } from "./schema.js";
|
|
6
|
+
import { redactSecrets } from "./discovery.js";
|
|
7
|
+
import { sourceValidationCoverageValues } from "./sourceStatus.js";
|
|
8
|
+
export const AGENT_ECONOMICS_RECEIPT_KIND = "aibill.agent_economics_receipt";
|
|
9
|
+
export const AGENT_ECONOMICS_RECEIPT_V0_VERSION = "0.1.0";
|
|
10
|
+
function decodesToSensitiveIdentifier(value) {
|
|
11
|
+
const decodedCandidates = [];
|
|
12
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
13
|
+
if (/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
14
|
+
try {
|
|
15
|
+
const decoded = Buffer.from(value, "base64url");
|
|
16
|
+
if (decoded.toString("base64url") === value) {
|
|
17
|
+
decodedCandidates.push(utf8Decoder.decode(decoded));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Invalid encodings are handled by the identifier grammar.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (value.length % 2 === 0 && /^[a-f0-9]+$/i.test(value)) {
|
|
25
|
+
try {
|
|
26
|
+
decodedCandidates.push(utf8Decoder.decode(Buffer.from(value, "hex")));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Binary identifiers are not interpreted as encoded receipt metadata.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return decodedCandidates.some((decoded) => {
|
|
33
|
+
const normalized = decoded
|
|
34
|
+
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
35
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
36
|
+
if (containsKnownSecret(decoded) || containsKnownSecret(normalized) ||
|
|
37
|
+
looksLikeAuthReferenceIdentifier(decoded) ||
|
|
38
|
+
looksLikeAuthReferenceIdentifier(normalized) ||
|
|
39
|
+
looksLikeUnsafeIdentifier(decoded) || looksLikeUnsafeIdentifier(normalized))
|
|
40
|
+
return true;
|
|
41
|
+
return normalized.includes("/") || normalized.includes("\\");
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function containsKnownSecret(value) {
|
|
45
|
+
if (redactSecrets(value) !== value)
|
|
46
|
+
return true;
|
|
47
|
+
const secretShapes = [
|
|
48
|
+
/sk-proj-[A-Za-z0-9_-]{20,}/i,
|
|
49
|
+
/sk-ant-api\d{2}-[A-Za-z0-9_-]{20,}/i,
|
|
50
|
+
/sk-[A-Za-z0-9_-]{20,}/i,
|
|
51
|
+
/helicone_[A-Za-z0-9_-]{16,}/i,
|
|
52
|
+
/gh[pousr]_[A-Za-z0-9]{20,}/i,
|
|
53
|
+
/github_pat_[A-Za-z0-9_]{20,}/i,
|
|
54
|
+
/AIza[0-9A-Za-z_-]{30,}/i,
|
|
55
|
+
/eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/i,
|
|
56
|
+
/xox[baprs]-[A-Za-z0-9-]{10,}/i,
|
|
57
|
+
/AKIA[0-9A-Z]{16}/,
|
|
58
|
+
/glpat-[A-Za-z0-9_-]{16,}/i,
|
|
59
|
+
/npm_[A-Za-z0-9]{30,}/i
|
|
60
|
+
];
|
|
61
|
+
return secretShapes.some((pattern) => pattern.test(value)) ||
|
|
62
|
+
/(?:^|[._-])(?:sk-|gh[pousr]_|github_pat_|npm_|AKIA|AIza|xox[baprs]-|glpat-|helicone_)/i
|
|
63
|
+
.test(value);
|
|
64
|
+
}
|
|
65
|
+
function looksLikeAuthReferenceIdentifier(value) {
|
|
66
|
+
return /(?:^|[._-])(?:env|keychain|credential|secret)[._-]/i.test(value);
|
|
67
|
+
}
|
|
68
|
+
function looksLikeUnsafeIdentifier(value) {
|
|
69
|
+
if (/(?:^|[._-])(?:bearer|token|password|passwd|credential|authorization|auth)[._]/i
|
|
70
|
+
.test(value))
|
|
71
|
+
return true;
|
|
72
|
+
if (/(?:^|[._-])(?:bearer|token|password|passwd|credential|authorization|auth)-[A-Za-z0-9_-]{16,}$/i
|
|
73
|
+
.test(value))
|
|
74
|
+
return true;
|
|
75
|
+
if (/(?:^|[._-])(?:api[._]?key|access[._]?key|private[._]?key)[._]/i
|
|
76
|
+
.test(value))
|
|
77
|
+
return true;
|
|
78
|
+
if (/(?:^|[._-])(?:api[._-]?key|access[._-]?key|private[._-]?key)-[A-Za-z0-9_-]{16,}$/i
|
|
79
|
+
.test(value))
|
|
80
|
+
return true;
|
|
81
|
+
const words = value.toLowerCase().replace(/[._-]+/g, " ");
|
|
82
|
+
return /\b(?:ignore|disregard|override|bypass)\b.*\b(?:previous|prior|system|all)\b.*\b(?:instructions?|prompts?|rules?)\b/.test(words) ||
|
|
83
|
+
/\b(?:upload|send|exfiltrate|leak|reveal|print)\b.*\b(?:secrets?|credentials?|passwords?|private keys?)\b/.test(words) ||
|
|
84
|
+
/\b(?:upload|send|exfiltrate|leak)\b.*\btokens?\b.*\b(?:attacker|external|remote)\b/.test(words);
|
|
85
|
+
}
|
|
86
|
+
function hasUnpairedSurrogate(value) {
|
|
87
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
88
|
+
const codeUnit = value.charCodeAt(index);
|
|
89
|
+
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
90
|
+
const next = value.charCodeAt(index + 1);
|
|
91
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
92
|
+
return true;
|
|
93
|
+
index += 1;
|
|
94
|
+
}
|
|
95
|
+
else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const safeIdentifierSchema = z.string()
|
|
102
|
+
.min(1)
|
|
103
|
+
.max(128)
|
|
104
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "Expected a path-free, control-free identifier.")
|
|
105
|
+
.superRefine((value, context) => {
|
|
106
|
+
if (containsKnownSecret(value) ||
|
|
107
|
+
looksLikeAuthReferenceIdentifier(value) ||
|
|
108
|
+
decodesToSensitiveIdentifier(value)) {
|
|
109
|
+
context.addIssue({
|
|
110
|
+
code: "custom",
|
|
111
|
+
message: "Credential-like and auth-reference identifiers are not receipt data."
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
const receiptIdentifierSchema = safeIdentifierSchema.superRefine((value, context) => {
|
|
116
|
+
if (looksLikeUnsafeIdentifier(value)) {
|
|
117
|
+
context.addIssue({
|
|
118
|
+
code: "custom",
|
|
119
|
+
message: "Credential references and prompt-like instructions are not receipt identifiers."
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
const receiptErrorCodeSchema = safeIdentifierSchema.superRefine((value, context) => {
|
|
124
|
+
const knownCredentialStatus = /^(?:token|password|passwd|credential|authorization|auth|api_key|access_key|private_key)_(?:expired|invalid|missing|revoked|unavailable|failed|required|denied)$/i
|
|
125
|
+
.test(value);
|
|
126
|
+
if (looksLikeUnsafeIdentifier(value) && !knownCredentialStatus) {
|
|
127
|
+
context.addIssue({
|
|
128
|
+
code: "custom",
|
|
129
|
+
message: "Credential references and prompt-like instructions are not receipt error codes."
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
const utcTimestampSchema = z.string()
|
|
134
|
+
.datetime({ offset: true })
|
|
135
|
+
.transform((value) => new Date(value).toISOString());
|
|
136
|
+
const usdSchema = z.number()
|
|
137
|
+
.finite()
|
|
138
|
+
.nonnegative()
|
|
139
|
+
.transform((value) => Object.is(value, -0) ? 0 : value);
|
|
140
|
+
const tokenCountSchema = z.number()
|
|
141
|
+
.int()
|
|
142
|
+
.nonnegative()
|
|
143
|
+
.transform((value) => Object.is(value, -0) ? 0 : value);
|
|
144
|
+
const financialEvidenceWithAmountSchema = costConfidenceSchema.exclude(["missing"]);
|
|
145
|
+
export const receiptModeValues = [
|
|
146
|
+
"local",
|
|
147
|
+
"connected",
|
|
148
|
+
"mixed",
|
|
149
|
+
"sample"
|
|
150
|
+
];
|
|
151
|
+
export const receiptModeSchema = z.enum(receiptModeValues);
|
|
152
|
+
export const receiptSourceKindValues = [
|
|
153
|
+
"local_agent_log",
|
|
154
|
+
"provider_billing_api",
|
|
155
|
+
"provider_usage_api",
|
|
156
|
+
"user_declaration",
|
|
157
|
+
"sample_fixture"
|
|
158
|
+
];
|
|
159
|
+
export const receiptSourceKindSchema = z.enum(receiptSourceKindValues);
|
|
160
|
+
export const receiptProvenanceOriginValues = [
|
|
161
|
+
"provider_reported",
|
|
162
|
+
"locally_observed",
|
|
163
|
+
"user_declared",
|
|
164
|
+
"sample"
|
|
165
|
+
];
|
|
166
|
+
export const receiptProvenanceOriginSchema = z.enum(receiptProvenanceOriginValues);
|
|
167
|
+
export const receiptTransformationValues = Object.freeze([
|
|
168
|
+
"normalized",
|
|
169
|
+
"aggregated",
|
|
170
|
+
"api_rate_estimated"
|
|
171
|
+
]);
|
|
172
|
+
export const receiptTransformationSchema = z.enum(receiptTransformationValues);
|
|
173
|
+
export const receiptAccountingBasisValues = [
|
|
174
|
+
"provider_billed",
|
|
175
|
+
"api_equivalent",
|
|
176
|
+
"user_declared"
|
|
177
|
+
];
|
|
178
|
+
export const receiptAccountingBasisSchema = z.enum(receiptAccountingBasisValues);
|
|
179
|
+
export const receiptMappingGapCodeValues = [
|
|
180
|
+
"cost_unsplit",
|
|
181
|
+
"cost_unpriced",
|
|
182
|
+
"model_unmapped",
|
|
183
|
+
"provider_unmapped",
|
|
184
|
+
"source_record_unmapped"
|
|
185
|
+
];
|
|
186
|
+
export const receiptMappingGapCodeSchema = z.enum(receiptMappingGapCodeValues);
|
|
187
|
+
export const receiptFreshnessSchema = z.object({
|
|
188
|
+
status: z.enum(["fresh", "stale", "not_checked", "error"]),
|
|
189
|
+
checkedAt: utcTimestampSchema.optional(),
|
|
190
|
+
latestEvidenceAt: utcTimestampSchema.optional(),
|
|
191
|
+
errorCode: receiptErrorCodeSchema.optional()
|
|
192
|
+
}).strict().superRefine((freshness, context) => {
|
|
193
|
+
if ((freshness.status === "fresh" || freshness.status === "stale") && !freshness.checkedAt) {
|
|
194
|
+
context.addIssue({
|
|
195
|
+
code: "custom",
|
|
196
|
+
path: ["checkedAt"],
|
|
197
|
+
message: "Fresh and stale source states require a check timestamp."
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (freshness.status === "error") {
|
|
201
|
+
if (!freshness.checkedAt) {
|
|
202
|
+
context.addIssue({
|
|
203
|
+
code: "custom",
|
|
204
|
+
path: ["checkedAt"],
|
|
205
|
+
message: "Error source states require the timestamp of the failed check."
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (!freshness.errorCode) {
|
|
209
|
+
context.addIssue({
|
|
210
|
+
code: "custom",
|
|
211
|
+
path: ["errorCode"],
|
|
212
|
+
message: "Error source states require a sanitized error code."
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (freshness.errorCode) {
|
|
217
|
+
context.addIssue({
|
|
218
|
+
code: "custom",
|
|
219
|
+
path: ["errorCode"],
|
|
220
|
+
message: "Only error source states may carry an error code."
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
if (freshness.status === "not_checked" &&
|
|
224
|
+
(freshness.checkedAt || freshness.latestEvidenceAt || freshness.errorCode)) {
|
|
225
|
+
context.addIssue({
|
|
226
|
+
code: "custom",
|
|
227
|
+
message: "A source that was not checked cannot carry observation or error metadata."
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (freshness.checkedAt && freshness.latestEvidenceAt &&
|
|
231
|
+
Date.parse(freshness.latestEvidenceAt) > Date.parse(freshness.checkedAt)) {
|
|
232
|
+
context.addIssue({
|
|
233
|
+
code: "custom",
|
|
234
|
+
path: ["latestEvidenceAt"],
|
|
235
|
+
message: "Latest evidence cannot postdate the source check."
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}).transform((freshness) => ({
|
|
239
|
+
status: freshness.status,
|
|
240
|
+
...(freshness.checkedAt !== undefined ? { checkedAt: freshness.checkedAt } : {}),
|
|
241
|
+
...(freshness.latestEvidenceAt !== undefined
|
|
242
|
+
? { latestEvidenceAt: freshness.latestEvidenceAt }
|
|
243
|
+
: {}),
|
|
244
|
+
...(freshness.errorCode !== undefined ? { errorCode: freshness.errorCode } : {})
|
|
245
|
+
}));
|
|
246
|
+
export const receiptSourceSchema = z.object({
|
|
247
|
+
id: receiptIdentifierSchema,
|
|
248
|
+
kind: receiptSourceKindSchema,
|
|
249
|
+
provider: receiptIdentifierSchema,
|
|
250
|
+
validationCoverage: z.enum(sourceValidationCoverageValues).default("untested"),
|
|
251
|
+
freshness: receiptFreshnessSchema
|
|
252
|
+
}).strict().superRefine((source, context) => {
|
|
253
|
+
if (source.kind === "sample_fixture" && source.validationCoverage === "live_verified") {
|
|
254
|
+
context.addIssue({
|
|
255
|
+
code: "custom",
|
|
256
|
+
path: ["validationCoverage"],
|
|
257
|
+
message: "Sample fixtures cannot claim live verification."
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
export const receiptSourceRecordReferenceSchema = z.object({
|
|
262
|
+
sourceId: receiptIdentifierSchema,
|
|
263
|
+
recordId: z.string().regex(/^ref_[a-f0-9]{32}$/)
|
|
264
|
+
}).strict();
|
|
265
|
+
const sourceNativeRecordIdSchema = z.string()
|
|
266
|
+
.min(1)
|
|
267
|
+
.max(4_096)
|
|
268
|
+
.superRefine((value, context) => {
|
|
269
|
+
if (hasUnpairedSurrogate(value)) {
|
|
270
|
+
context.addIssue({
|
|
271
|
+
code: "custom",
|
|
272
|
+
message: "Source-native record IDs cannot contain unpaired Unicode surrogates."
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
/** Convert a source-native identifier into the only reference form persisted in a receipt. */
|
|
277
|
+
export function createReceiptSourceRecordReference(sourceId, sourceRecordId) {
|
|
278
|
+
const parsedSourceId = receiptIdentifierSchema.parse(sourceId);
|
|
279
|
+
const parsedRecordId = sourceNativeRecordIdSchema.parse(sourceRecordId);
|
|
280
|
+
const opaqueId = createHash("sha256")
|
|
281
|
+
.update(`${parsedSourceId}\u0000${parsedRecordId}`)
|
|
282
|
+
.digest("hex")
|
|
283
|
+
.slice(0, 32);
|
|
284
|
+
return receiptSourceRecordReferenceSchema.parse({
|
|
285
|
+
sourceId: parsedSourceId,
|
|
286
|
+
recordId: `ref_${opaqueId}`
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
export const receiptProvenanceSchema = z.object({
|
|
290
|
+
origin: receiptProvenanceOriginSchema,
|
|
291
|
+
transformations: z.array(receiptTransformationSchema).min(1).max(3)
|
|
292
|
+
}).strict().superRefine((provenance, context) => {
|
|
293
|
+
if (new Set(provenance.transformations).size !== provenance.transformations.length) {
|
|
294
|
+
context.addIssue({
|
|
295
|
+
code: "custom",
|
|
296
|
+
path: ["transformations"],
|
|
297
|
+
message: "Receipt transformations must be unique."
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
const receiptLineBaseShape = {
|
|
302
|
+
id: receiptIdentifierSchema,
|
|
303
|
+
sourceId: receiptIdentifierSchema,
|
|
304
|
+
observedAt: utcTimestampSchema,
|
|
305
|
+
granularity: usageGranularitySchema,
|
|
306
|
+
provenance: receiptProvenanceSchema,
|
|
307
|
+
sourceRecordReferences: z.array(receiptSourceRecordReferenceSchema).min(1).max(256)
|
|
308
|
+
};
|
|
309
|
+
export const receiptTokenUsageLineSchema = z.object({
|
|
310
|
+
...receiptLineBaseShape,
|
|
311
|
+
kind: z.literal("token_usage"),
|
|
312
|
+
provider: receiptIdentifierSchema,
|
|
313
|
+
model: receiptIdentifierSchema,
|
|
314
|
+
requestedModel: receiptIdentifierSchema.optional(),
|
|
315
|
+
inputTokens: tokenCountSchema,
|
|
316
|
+
outputTokens: tokenCountSchema
|
|
317
|
+
}).strict().superRefine((line, context) => {
|
|
318
|
+
if (line.provenance.transformations.includes("api_rate_estimated")) {
|
|
319
|
+
context.addIssue({
|
|
320
|
+
code: "custom",
|
|
321
|
+
path: ["provenance", "transformations"],
|
|
322
|
+
message: "Token usage cannot carry an API-rate cost transformation."
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
export const receiptFinancialCostLineSchema = z.object({
|
|
327
|
+
...receiptLineBaseShape,
|
|
328
|
+
kind: z.literal("financial_cost"),
|
|
329
|
+
amountUsd: usdSchema,
|
|
330
|
+
currency: z.literal("USD"),
|
|
331
|
+
accountingBasis: receiptAccountingBasisSchema,
|
|
332
|
+
financialEvidence: financialEvidenceWithAmountSchema
|
|
333
|
+
}).strict().superRefine((line, context) => {
|
|
334
|
+
const transformations = new Set(line.provenance.transformations);
|
|
335
|
+
if (line.accountingBasis === "provider_billed" &&
|
|
336
|
+
(line.financialEvidence !== "verified" || line.provenance.origin !== "provider_reported")) {
|
|
337
|
+
context.addIssue({
|
|
338
|
+
code: "custom",
|
|
339
|
+
path: ["accountingBasis"],
|
|
340
|
+
message: "Provider-billed cost requires verified, provider-reported evidence."
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (line.accountingBasis === "api_equivalent" &&
|
|
344
|
+
(line.financialEvidence !== "estimated" || !transformations.has("api_rate_estimated"))) {
|
|
345
|
+
context.addIssue({
|
|
346
|
+
code: "custom",
|
|
347
|
+
path: ["accountingBasis"],
|
|
348
|
+
message: "API-equivalent cost must remain estimated and name its rate transformation."
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
if (line.accountingBasis === "user_declared" &&
|
|
352
|
+
(line.financialEvidence === "verified" || line.provenance.origin !== "user_declared")) {
|
|
353
|
+
context.addIssue({
|
|
354
|
+
code: "custom",
|
|
355
|
+
path: ["accountingBasis"],
|
|
356
|
+
message: "User-declared cost cannot become verified provider billing."
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (line.accountingBasis !== "api_equivalent" && transformations.has("api_rate_estimated")) {
|
|
360
|
+
context.addIssue({
|
|
361
|
+
code: "custom",
|
|
362
|
+
path: ["provenance", "transformations"],
|
|
363
|
+
message: "Only API-equivalent cost may use the API-rate transformation."
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
export const receiptLineSchema = z.discriminatedUnion("kind", [
|
|
368
|
+
receiptTokenUsageLineSchema,
|
|
369
|
+
receiptFinancialCostLineSchema
|
|
370
|
+
]);
|
|
371
|
+
export const receiptMappingGapSchema = z.object({
|
|
372
|
+
code: receiptMappingGapCodeSchema,
|
|
373
|
+
lineId: receiptIdentifierSchema.optional(),
|
|
374
|
+
sourceId: receiptIdentifierSchema.optional()
|
|
375
|
+
}).strict().transform((gap) => ({
|
|
376
|
+
code: gap.code,
|
|
377
|
+
...(gap.lineId !== undefined ? { lineId: gap.lineId } : {}),
|
|
378
|
+
...(gap.sourceId !== undefined ? { sourceId: gap.sourceId } : {})
|
|
379
|
+
}));
|
|
380
|
+
export const receiptCostTotalSchema = z.object({
|
|
381
|
+
accountingBasis: receiptAccountingBasisSchema,
|
|
382
|
+
financialEvidence: financialEvidenceWithAmountSchema,
|
|
383
|
+
currency: z.literal("USD"),
|
|
384
|
+
amountUsd: usdSchema,
|
|
385
|
+
lineCount: z.number().int().positive()
|
|
386
|
+
}).strict();
|
|
387
|
+
const receiptBodyObjectSchema = z.object({
|
|
388
|
+
kind: z.literal(AGENT_ECONOMICS_RECEIPT_KIND),
|
|
389
|
+
schemaVersion: z.literal(AGENT_ECONOMICS_RECEIPT_V0_VERSION),
|
|
390
|
+
generatedAt: utcTimestampSchema,
|
|
391
|
+
mode: receiptModeSchema,
|
|
392
|
+
demoOnly: z.boolean(),
|
|
393
|
+
window: z.object({
|
|
394
|
+
start: utcTimestampSchema,
|
|
395
|
+
end: utcTimestampSchema
|
|
396
|
+
}).strict(),
|
|
397
|
+
sources: z.array(receiptSourceSchema).min(1).max(64),
|
|
398
|
+
lines: z.array(receiptLineSchema).max(10_000),
|
|
399
|
+
mappingGaps: z.array(receiptMappingGapSchema).max(10_000)
|
|
400
|
+
}).strict();
|
|
401
|
+
function validateReceiptBody(receipt, context) {
|
|
402
|
+
if (Date.parse(receipt.window.start) >= Date.parse(receipt.window.end)) {
|
|
403
|
+
context.addIssue({
|
|
404
|
+
code: "custom",
|
|
405
|
+
path: ["window", "end"],
|
|
406
|
+
message: "Receipt window end must follow its start."
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (Date.parse(receipt.generatedAt) < Date.parse(receipt.window.end)) {
|
|
410
|
+
context.addIssue({
|
|
411
|
+
code: "custom",
|
|
412
|
+
path: ["generatedAt"],
|
|
413
|
+
message: "A receipt cannot be generated before its evidence window closes."
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if ((receipt.mode === "sample") !== receipt.demoOnly) {
|
|
417
|
+
context.addIssue({
|
|
418
|
+
code: "custom",
|
|
419
|
+
path: ["demoOnly"],
|
|
420
|
+
message: "Sample mode and the demo-only boundary must agree."
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
const sourceIds = receipt.sources.map((source) => source.id);
|
|
424
|
+
const sourceIdSet = new Set(sourceIds);
|
|
425
|
+
const sourceById = new Map(receipt.sources.map((source) => [source.id, source]));
|
|
426
|
+
if (sourceIdSet.size !== sourceIds.length) {
|
|
427
|
+
context.addIssue({ code: "custom", path: ["sources"], message: "Source IDs must be unique." });
|
|
428
|
+
}
|
|
429
|
+
const hasLocalSource = receipt.sources.some((source) => source.kind === "local_agent_log" || source.kind === "user_declaration");
|
|
430
|
+
const hasConnectedSource = receipt.sources.some((source) => source.kind === "provider_billing_api" || source.kind === "provider_usage_api");
|
|
431
|
+
if ((receipt.mode === "local" && (!hasLocalSource || hasConnectedSource)) ||
|
|
432
|
+
(receipt.mode === "connected" && (!hasConnectedSource || hasLocalSource)) ||
|
|
433
|
+
(receipt.mode === "mixed" && (!hasLocalSource || !hasConnectedSource))) {
|
|
434
|
+
context.addIssue({
|
|
435
|
+
code: "custom",
|
|
436
|
+
path: ["mode"],
|
|
437
|
+
message: "Receipt mode must match its local and connected source classes."
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const lineIds = receipt.lines.map((line) => line.id);
|
|
441
|
+
const lineIdSet = new Set(lineIds);
|
|
442
|
+
const lineById = new Map(receipt.lines.map((line) => [line.id, line]));
|
|
443
|
+
if (lineIdSet.size !== lineIds.length) {
|
|
444
|
+
context.addIssue({ code: "custom", path: ["lines"], message: "Line IDs must be unique." });
|
|
445
|
+
}
|
|
446
|
+
const generatedAt = Date.parse(receipt.generatedAt);
|
|
447
|
+
receipt.sources.forEach((source, sourceIndex) => {
|
|
448
|
+
if (source.freshness.checkedAt && Date.parse(source.freshness.checkedAt) > generatedAt) {
|
|
449
|
+
context.addIssue({
|
|
450
|
+
code: "custom",
|
|
451
|
+
path: ["sources", sourceIndex, "freshness", "checkedAt"],
|
|
452
|
+
message: "A source check cannot postdate receipt generation."
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
if (source.freshness.latestEvidenceAt &&
|
|
456
|
+
Date.parse(source.freshness.latestEvidenceAt) > generatedAt) {
|
|
457
|
+
context.addIssue({
|
|
458
|
+
code: "custom",
|
|
459
|
+
path: ["sources", sourceIndex, "freshness", "latestEvidenceAt"],
|
|
460
|
+
message: "Latest source evidence cannot postdate receipt generation."
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
receipt.lines.forEach((line, lineIndex) => {
|
|
465
|
+
const source = sourceById.get(line.sourceId);
|
|
466
|
+
if (!sourceIdSet.has(line.sourceId)) {
|
|
467
|
+
context.addIssue({
|
|
468
|
+
code: "custom",
|
|
469
|
+
path: ["lines", lineIndex, "sourceId"],
|
|
470
|
+
message: "Receipt lines must reference a declared source."
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
if (source) {
|
|
474
|
+
if (source.freshness.status === "not_checked" &&
|
|
475
|
+
source.kind !== "user_declaration" &&
|
|
476
|
+
source.kind !== "sample_fixture") {
|
|
477
|
+
context.addIssue({
|
|
478
|
+
code: "custom",
|
|
479
|
+
path: ["lines", lineIndex, "sourceId"],
|
|
480
|
+
message: "Observed lines require a checked source; user declarations and sample fixtures are explicit exceptions."
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
const expectedOrigin = source.kind === "local_agent_log"
|
|
484
|
+
? "locally_observed"
|
|
485
|
+
: source.kind === "user_declaration"
|
|
486
|
+
? "user_declared"
|
|
487
|
+
: source.kind === "sample_fixture"
|
|
488
|
+
? "sample"
|
|
489
|
+
: "provider_reported";
|
|
490
|
+
if (line.provenance.origin !== expectedOrigin) {
|
|
491
|
+
context.addIssue({
|
|
492
|
+
code: "custom",
|
|
493
|
+
path: ["lines", lineIndex, "provenance", "origin"],
|
|
494
|
+
message: "Line provenance must agree with the declared source kind."
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
if (line.kind === "token_usage" && line.provider !== source.provider) {
|
|
498
|
+
context.addIssue({
|
|
499
|
+
code: "custom",
|
|
500
|
+
path: ["lines", lineIndex, "provider"],
|
|
501
|
+
message: "Token provider identity must agree with its declared source."
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (line.kind === "financial_cost" &&
|
|
505
|
+
line.accountingBasis === "provider_billed" &&
|
|
506
|
+
source.kind !== "provider_billing_api") {
|
|
507
|
+
context.addIssue({
|
|
508
|
+
code: "custom",
|
|
509
|
+
path: ["lines", lineIndex, "accountingBasis"],
|
|
510
|
+
message: "Provider-billed cost requires a provider billing source."
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
if (line.kind === "financial_cost" &&
|
|
514
|
+
line.accountingBasis === "user_declared" &&
|
|
515
|
+
source.kind !== "user_declaration") {
|
|
516
|
+
context.addIssue({
|
|
517
|
+
code: "custom",
|
|
518
|
+
path: ["lines", lineIndex, "accountingBasis"],
|
|
519
|
+
message: "User-declared cost requires a user-declaration source."
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const referenceKeys = line.sourceRecordReferences.map((reference) => `${reference.sourceId}\u0000${reference.recordId}`);
|
|
524
|
+
if (new Set(referenceKeys).size !== referenceKeys.length) {
|
|
525
|
+
context.addIssue({
|
|
526
|
+
code: "custom",
|
|
527
|
+
path: ["lines", lineIndex, "sourceRecordReferences"],
|
|
528
|
+
message: "Source-record references must be unique."
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
line.sourceRecordReferences.forEach((reference, referenceIndex) => {
|
|
532
|
+
if (reference.sourceId !== line.sourceId || !sourceIdSet.has(reference.sourceId)) {
|
|
533
|
+
context.addIssue({
|
|
534
|
+
code: "custom",
|
|
535
|
+
path: ["lines", lineIndex, "sourceRecordReferences", referenceIndex],
|
|
536
|
+
message: "A source-record reference must resolve to the line's declared source."
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
const observedAt = Date.parse(line.observedAt);
|
|
541
|
+
if (observedAt < Date.parse(receipt.window.start) || observedAt > Date.parse(receipt.window.end)) {
|
|
542
|
+
context.addIssue({
|
|
543
|
+
code: "custom",
|
|
544
|
+
path: ["lines", lineIndex, "observedAt"],
|
|
545
|
+
message: "Receipt line evidence must fall inside the receipt window."
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (source?.freshness.checkedAt && observedAt > Date.parse(source.freshness.checkedAt)) {
|
|
549
|
+
context.addIssue({
|
|
550
|
+
code: "custom",
|
|
551
|
+
path: ["lines", lineIndex, "observedAt"],
|
|
552
|
+
message: "Line evidence cannot postdate its source check."
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (source?.freshness.latestEvidenceAt &&
|
|
556
|
+
observedAt > Date.parse(source.freshness.latestEvidenceAt)) {
|
|
557
|
+
context.addIssue({
|
|
558
|
+
code: "custom",
|
|
559
|
+
path: ["lines", lineIndex, "observedAt"],
|
|
560
|
+
message: "Line evidence cannot postdate its source's latest-evidence marker."
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
if (receipt.mode === "sample") {
|
|
564
|
+
if (line.provenance.origin !== "sample") {
|
|
565
|
+
context.addIssue({
|
|
566
|
+
code: "custom",
|
|
567
|
+
path: ["lines", lineIndex, "provenance", "origin"],
|
|
568
|
+
message: "Sample receipts may contain only sample provenance."
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
if (line.kind === "financial_cost" && line.financialEvidence === "verified") {
|
|
572
|
+
context.addIssue({
|
|
573
|
+
code: "custom",
|
|
574
|
+
path: ["lines", lineIndex, "financialEvidence"],
|
|
575
|
+
message: "Sample receipts cannot carry verified financial evidence."
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
if (receipt.mode === "sample" &&
|
|
581
|
+
receipt.sources.some((source) => source.kind !== "sample_fixture")) {
|
|
582
|
+
context.addIssue({
|
|
583
|
+
code: "custom",
|
|
584
|
+
path: ["sources"],
|
|
585
|
+
message: "Sample receipts may contain only sample-fixture sources."
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
if (receipt.mode !== "sample" &&
|
|
589
|
+
receipt.sources.some((source) => source.kind === "sample_fixture")) {
|
|
590
|
+
context.addIssue({
|
|
591
|
+
code: "custom",
|
|
592
|
+
path: ["sources"],
|
|
593
|
+
message: "Sample-fixture sources cannot enter a non-demo receipt."
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
receipt.mappingGaps.forEach((gap, gapIndex) => {
|
|
597
|
+
if (gap.lineId && !lineIdSet.has(gap.lineId)) {
|
|
598
|
+
context.addIssue({
|
|
599
|
+
code: "custom",
|
|
600
|
+
path: ["mappingGaps", gapIndex, "lineId"],
|
|
601
|
+
message: "Mapping gaps must reference a declared line."
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
if (gap.sourceId && !sourceIdSet.has(gap.sourceId)) {
|
|
605
|
+
context.addIssue({
|
|
606
|
+
code: "custom",
|
|
607
|
+
path: ["mappingGaps", gapIndex, "sourceId"],
|
|
608
|
+
message: "Mapping gaps must reference a declared source."
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
if (gap.lineId && gap.sourceId && lineById.get(gap.lineId)?.sourceId !== gap.sourceId) {
|
|
612
|
+
context.addIssue({
|
|
613
|
+
code: "custom",
|
|
614
|
+
path: ["mappingGaps", gapIndex, "sourceId"],
|
|
615
|
+
message: "A mapping gap's source must agree with its referenced line."
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
if (new Set(receipt.mappingGaps.map(mappingGapKey)).size !== receipt.mappingGaps.length) {
|
|
620
|
+
context.addIssue({
|
|
621
|
+
code: "custom",
|
|
622
|
+
path: ["mappingGaps"],
|
|
623
|
+
message: "Mapping gaps must be unique."
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
const totalDerivation = deriveCostTotals(receipt.lines);
|
|
627
|
+
if (!totalDerivation.success) {
|
|
628
|
+
context.addIssue({
|
|
629
|
+
code: "custom",
|
|
630
|
+
path: ["lines"],
|
|
631
|
+
message: totalDerivation.error
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
export const agentEconomicsReceiptV0DraftSchema = receiptBodyObjectSchema
|
|
636
|
+
.superRefine(validateReceiptBody);
|
|
637
|
+
const receiptWithComputedFieldsObjectSchema = receiptBodyObjectSchema.extend({
|
|
638
|
+
id: z.string().regex(/^aer_v0_[a-f0-9]{64}$/),
|
|
639
|
+
costTotals: z.array(receiptCostTotalSchema).max(9)
|
|
640
|
+
});
|
|
641
|
+
function canonicalReceiptBody(receipt) {
|
|
642
|
+
const transformationOrder = new Map(receiptTransformationValues.map((value, index) => [value, index]));
|
|
643
|
+
const lines = receipt.lines.map((line) => {
|
|
644
|
+
const canonicalLine = {
|
|
645
|
+
...line,
|
|
646
|
+
provenance: {
|
|
647
|
+
...line.provenance,
|
|
648
|
+
transformations: [...line.provenance.transformations].sort((left, right) => (transformationOrder.get(left) ?? 0) -
|
|
649
|
+
(transformationOrder.get(right) ?? 0))
|
|
650
|
+
},
|
|
651
|
+
sourceRecordReferences: [...line.sourceRecordReferences].sort(compareReferences)
|
|
652
|
+
};
|
|
653
|
+
if (canonicalLine.kind === "token_usage") {
|
|
654
|
+
const { requestedModel, ...withoutRequestedModel } = canonicalLine;
|
|
655
|
+
return {
|
|
656
|
+
...withoutRequestedModel,
|
|
657
|
+
...(requestedModel !== undefined ? { requestedModel } : {})
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
return canonicalLine;
|
|
661
|
+
}).sort((left, right) => compareText(left.id, right.id));
|
|
662
|
+
return {
|
|
663
|
+
kind: receipt.kind,
|
|
664
|
+
schemaVersion: receipt.schemaVersion,
|
|
665
|
+
generatedAt: receipt.generatedAt,
|
|
666
|
+
mode: receipt.mode,
|
|
667
|
+
demoOnly: receipt.demoOnly,
|
|
668
|
+
window: receipt.window,
|
|
669
|
+
sources: [...receipt.sources].sort((left, right) => compareText(left.id, right.id)),
|
|
670
|
+
lines,
|
|
671
|
+
mappingGaps: [...receipt.mappingGaps].sort((left, right) => compareText(mappingGapKey(left), mappingGapKey(right)))
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function compareReferences(left, right) {
|
|
675
|
+
return compareText(`${left.sourceId}\u0000${left.recordId}`, `${right.sourceId}\u0000${right.recordId}`);
|
|
676
|
+
}
|
|
677
|
+
function compareText(left, right) {
|
|
678
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
679
|
+
}
|
|
680
|
+
function mappingGapKey(gap) {
|
|
681
|
+
return `${gap.code}\u0000${gap.sourceId ?? ""}\u0000${gap.lineId ?? ""}`;
|
|
682
|
+
}
|
|
683
|
+
function deriveCostTotals(lines) {
|
|
684
|
+
const totals = new Map();
|
|
685
|
+
for (const line of lines) {
|
|
686
|
+
if (line.kind !== "financial_cost")
|
|
687
|
+
continue;
|
|
688
|
+
const key = `${line.accountingBasis}\u0000${line.financialEvidence}\u0000${line.currency}`;
|
|
689
|
+
const existing = totals.get(key);
|
|
690
|
+
const next = decimalParts(line.amountUsd);
|
|
691
|
+
if (!next)
|
|
692
|
+
return { success: false, error: "USD values must be finite, nonnegative decimal numbers." };
|
|
693
|
+
if (!existing) {
|
|
694
|
+
totals.set(key, {
|
|
695
|
+
accountingBasis: line.accountingBasis,
|
|
696
|
+
financialEvidence: line.financialEvidence,
|
|
697
|
+
currency: line.currency,
|
|
698
|
+
coefficient: next.coefficient,
|
|
699
|
+
scale: next.scale,
|
|
700
|
+
lineCount: 1
|
|
701
|
+
});
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
const commonScale = Math.max(existing.scale, next.scale);
|
|
705
|
+
existing.coefficient =
|
|
706
|
+
existing.coefficient * (10n ** BigInt(commonScale - existing.scale)) +
|
|
707
|
+
next.coefficient * (10n ** BigInt(commonScale - next.scale));
|
|
708
|
+
existing.scale = commonScale;
|
|
709
|
+
existing.lineCount += 1;
|
|
710
|
+
}
|
|
711
|
+
const costTotals = [];
|
|
712
|
+
for (const total of totals.values()) {
|
|
713
|
+
const amountUsd = decimalPartsToNumber({
|
|
714
|
+
coefficient: total.coefficient,
|
|
715
|
+
scale: total.scale
|
|
716
|
+
});
|
|
717
|
+
if (amountUsd === undefined) {
|
|
718
|
+
return {
|
|
719
|
+
success: false,
|
|
720
|
+
error: "Aggregated USD total cannot be represented as a finite number without precision loss."
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
costTotals.push({
|
|
724
|
+
accountingBasis: total.accountingBasis,
|
|
725
|
+
financialEvidence: total.financialEvidence,
|
|
726
|
+
currency: total.currency,
|
|
727
|
+
amountUsd,
|
|
728
|
+
lineCount: total.lineCount
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
success: true,
|
|
733
|
+
costTotals: costTotals.sort((left, right) => compareText(totalKey(left), totalKey(right)))
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function totalKey(total) {
|
|
737
|
+
return `${total.accountingBasis}\u0000${total.financialEvidence}\u0000${total.currency}`;
|
|
738
|
+
}
|
|
739
|
+
function decimalPartsToNumber(parts) {
|
|
740
|
+
const digits = parts.coefficient.toString();
|
|
741
|
+
const decimal = parts.scale <= 0
|
|
742
|
+
? `${digits}${"0".repeat(-parts.scale)}`
|
|
743
|
+
: digits.length <= parts.scale
|
|
744
|
+
? `0.${"0".repeat(parts.scale - digits.length)}${digits}`
|
|
745
|
+
: `${digits.slice(0, -parts.scale)}.${digits.slice(-parts.scale)}`;
|
|
746
|
+
const total = Number(decimal);
|
|
747
|
+
const exact = normalizeDecimalParts(parts);
|
|
748
|
+
if (!Number.isFinite(total))
|
|
749
|
+
return undefined;
|
|
750
|
+
const roundTripParts = decimalParts(total);
|
|
751
|
+
if (!roundTripParts)
|
|
752
|
+
return undefined;
|
|
753
|
+
const roundTrip = normalizeDecimalParts(roundTripParts);
|
|
754
|
+
if (roundTrip.coefficient !== exact.coefficient || roundTrip.scale !== exact.scale) {
|
|
755
|
+
return undefined;
|
|
756
|
+
}
|
|
757
|
+
return Object.is(total, -0) ? 0 : total;
|
|
758
|
+
}
|
|
759
|
+
function decimalParts(value) {
|
|
760
|
+
const match = /^(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/i.exec(value.toString());
|
|
761
|
+
if (!match)
|
|
762
|
+
return undefined;
|
|
763
|
+
const fraction = match[2] ?? "";
|
|
764
|
+
const exponent = Number(match[3] ?? "0");
|
|
765
|
+
return {
|
|
766
|
+
coefficient: BigInt(`${match[1]}${fraction}`),
|
|
767
|
+
scale: fraction.length - exponent
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function normalizeDecimalParts(parts) {
|
|
771
|
+
let { coefficient, scale } = parts;
|
|
772
|
+
if (coefficient === 0n)
|
|
773
|
+
return { coefficient: 0n, scale: 0 };
|
|
774
|
+
while (coefficient % 10n === 0n) {
|
|
775
|
+
coefficient /= 10n;
|
|
776
|
+
scale -= 1;
|
|
777
|
+
}
|
|
778
|
+
return { coefficient, scale };
|
|
779
|
+
}
|
|
780
|
+
function receiptDigest(body, costTotals) {
|
|
781
|
+
return `aer_v0_${createHash("sha256")
|
|
782
|
+
.update(canonicalJson({ ...body, costTotals }))
|
|
783
|
+
.digest("hex")}`;
|
|
784
|
+
}
|
|
785
|
+
function canonicalJson(value) {
|
|
786
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
787
|
+
return JSON.stringify(value);
|
|
788
|
+
}
|
|
789
|
+
if (typeof value === "number") {
|
|
790
|
+
if (!Number.isFinite(value))
|
|
791
|
+
throw new TypeError("Canonical receipt values must be finite.");
|
|
792
|
+
return JSON.stringify(value);
|
|
793
|
+
}
|
|
794
|
+
if (Array.isArray(value)) {
|
|
795
|
+
return `[${value.map((entry) => canonicalJson(entry)).join(",")}]`;
|
|
796
|
+
}
|
|
797
|
+
if (typeof value === "object" && value) {
|
|
798
|
+
const object = value;
|
|
799
|
+
return `{${Object.keys(object).filter((key) => object[key] !== undefined).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`).join(",")}}`;
|
|
800
|
+
}
|
|
801
|
+
throw new TypeError("Canonical receipt values must be JSON data.");
|
|
802
|
+
}
|
|
803
|
+
function validateComputedFields(receipt, context) {
|
|
804
|
+
validateReceiptBody(receipt, context);
|
|
805
|
+
const canonicalBody = canonicalReceiptBody(receipt);
|
|
806
|
+
const derivation = deriveCostTotals(canonicalBody.lines);
|
|
807
|
+
if (!derivation.success)
|
|
808
|
+
return;
|
|
809
|
+
const expectedTotals = derivation.costTotals;
|
|
810
|
+
const actualTotals = [...receipt.costTotals].sort((left, right) => compareText(totalKey(left), totalKey(right)));
|
|
811
|
+
if (new Set(actualTotals.map(totalKey)).size !== actualTotals.length ||
|
|
812
|
+
canonicalJson(actualTotals) !== canonicalJson(expectedTotals)) {
|
|
813
|
+
context.addIssue({
|
|
814
|
+
code: "custom",
|
|
815
|
+
path: ["costTotals"],
|
|
816
|
+
message: "Receipt cost totals must exactly match grouped financial-cost lines."
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
const expectedId = receiptDigest(canonicalBody, expectedTotals);
|
|
820
|
+
if (receipt.id !== expectedId) {
|
|
821
|
+
context.addIssue({
|
|
822
|
+
code: "custom",
|
|
823
|
+
path: ["id"],
|
|
824
|
+
message: "Receipt ID does not match its canonical SHA-256 digest."
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
export const agentEconomicsReceiptV0Schema = receiptWithComputedFieldsObjectSchema
|
|
829
|
+
.superRefine(validateComputedFields);
|
|
830
|
+
/** Build a canonical, content-addressed v0 receipt from validated evidence lines. */
|
|
831
|
+
export function createAgentEconomicsReceiptV0(input) {
|
|
832
|
+
const body = canonicalReceiptBody(agentEconomicsReceiptV0DraftSchema.parse(input));
|
|
833
|
+
const derivation = deriveCostTotals(body.lines);
|
|
834
|
+
const costTotals = derivation.success ? derivation.costTotals : [];
|
|
835
|
+
return agentEconomicsReceiptV0Schema.parse({
|
|
836
|
+
...body,
|
|
837
|
+
id: receiptDigest(body, costTotals),
|
|
838
|
+
costTotals
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
/** Parse and re-canonicalize a serialized receipt, rejecting stale IDs or totals. */
|
|
842
|
+
export function parseAgentEconomicsReceiptV0(value) {
|
|
843
|
+
const parsed = agentEconomicsReceiptV0Schema.parse(value);
|
|
844
|
+
const body = canonicalReceiptBody(parsed);
|
|
845
|
+
const derivation = deriveCostTotals(body.lines);
|
|
846
|
+
const costTotals = derivation.success ? derivation.costTotals : [];
|
|
847
|
+
return {
|
|
848
|
+
...body,
|
|
849
|
+
id: receiptDigest(body, costTotals),
|
|
850
|
+
costTotals
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
export const FOCUS_1_4_PIN = Object.freeze({
|
|
854
|
+
standard: "FOCUS",
|
|
855
|
+
version: "1.4",
|
|
856
|
+
status: "ratified"
|
|
857
|
+
});
|
|
858
|
+
export const FOCUS_1_5_WORKING_DRAFT_PIN = Object.freeze({
|
|
859
|
+
standard: "FOCUS",
|
|
860
|
+
version: "1.5",
|
|
861
|
+
status: "working_draft",
|
|
862
|
+
draftAsOf: "2026-08-08"
|
|
863
|
+
});
|
|
864
|
+
export const focusProjectionTargetSchema = z.enum([
|
|
865
|
+
"focus_1_4",
|
|
866
|
+
"focus_1_5_working_draft"
|
|
867
|
+
]);
|
|
868
|
+
/**
|
|
869
|
+
* Version-pinned FOCUS projection. It deliberately keeps estimates out of
|
|
870
|
+
* BilledCost and treats pre-1.5 token fields as aibill extensions.
|
|
871
|
+
*/
|
|
872
|
+
export function projectAgentEconomicsReceiptV0ToFocus(value, target) {
|
|
873
|
+
const receipt = parseAgentEconomicsReceiptV0(value);
|
|
874
|
+
const parsedTarget = focusProjectionTargetSchema.parse(target);
|
|
875
|
+
const sources = new Map(receipt.sources.map((source) => [source.id, source]));
|
|
876
|
+
const gaps = receipt.mappingGaps.map((gap) => ({ ...gap }));
|
|
877
|
+
const rows = [];
|
|
878
|
+
for (const line of receipt.lines) {
|
|
879
|
+
if (line.kind === "token_usage") {
|
|
880
|
+
const mapping = parsedTarget === "focus_1_4"
|
|
881
|
+
? "aibill_extension"
|
|
882
|
+
: "focus_1_5_working_draft";
|
|
883
|
+
rows.push({
|
|
884
|
+
kind: "token_usage",
|
|
885
|
+
rowId: `focus.token.${line.id}.input`,
|
|
886
|
+
sourceLineId: line.id,
|
|
887
|
+
provider: line.provider,
|
|
888
|
+
model: line.model,
|
|
889
|
+
ConsumedQuantity: line.inputTokens,
|
|
890
|
+
ConsumedUnit: "input_token",
|
|
891
|
+
mapping,
|
|
892
|
+
sourceRecordReferences: line.sourceRecordReferences
|
|
893
|
+
}, {
|
|
894
|
+
kind: "token_usage",
|
|
895
|
+
rowId: `focus.token.${line.id}.output`,
|
|
896
|
+
sourceLineId: line.id,
|
|
897
|
+
provider: line.provider,
|
|
898
|
+
model: line.model,
|
|
899
|
+
ConsumedQuantity: line.outputTokens,
|
|
900
|
+
ConsumedUnit: "output_token",
|
|
901
|
+
mapping,
|
|
902
|
+
sourceRecordReferences: line.sourceRecordReferences
|
|
903
|
+
});
|
|
904
|
+
if (parsedTarget === "focus_1_4") {
|
|
905
|
+
gaps.push({ code: "focus_1_4_token_dimensions_unavailable", lineId: line.id });
|
|
906
|
+
}
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
const provider = sources.get(line.sourceId)?.provider;
|
|
910
|
+
if (!provider)
|
|
911
|
+
throw new TypeError("Validated receipt source unexpectedly disappeared.");
|
|
912
|
+
rows.push({
|
|
913
|
+
kind: "financial_cost",
|
|
914
|
+
rowId: `focus.cost.${line.id}`,
|
|
915
|
+
sourceLineId: line.id,
|
|
916
|
+
provider,
|
|
917
|
+
BillingCurrency: line.currency,
|
|
918
|
+
BilledCost: line.accountingBasis === "provider_billed" ? line.amountUsd : null,
|
|
919
|
+
extensions: line.accountingBasis === "api_equivalent"
|
|
920
|
+
? { "x_aibill.api_equivalent_cost_usd": line.amountUsd }
|
|
921
|
+
: line.accountingBasis === "user_declared"
|
|
922
|
+
? { "x_aibill.user_declared_cost_usd": line.amountUsd }
|
|
923
|
+
: {},
|
|
924
|
+
accountingBasis: line.accountingBasis,
|
|
925
|
+
financialEvidence: line.financialEvidence,
|
|
926
|
+
sourceRecordReferences: line.sourceRecordReferences
|
|
927
|
+
});
|
|
928
|
+
if (line.accountingBasis === "api_equivalent") {
|
|
929
|
+
gaps.push({ code: "api_equivalent_not_billed_cost", lineId: line.id });
|
|
930
|
+
}
|
|
931
|
+
else if (line.accountingBasis === "user_declared") {
|
|
932
|
+
gaps.push({ code: "user_declared_not_billed_cost", lineId: line.id });
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
target: parsedTarget === "focus_1_4" ? FOCUS_1_4_PIN : FOCUS_1_5_WORKING_DRAFT_PIN,
|
|
937
|
+
receiptId: receipt.id,
|
|
938
|
+
rows,
|
|
939
|
+
gaps: uniqueSortedGaps(gaps)
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
export const OTEL_GENAI_DEVELOPMENT_PIN = Object.freeze({
|
|
943
|
+
standard: "OpenTelemetry GenAI semantic conventions",
|
|
944
|
+
version: "development-2026-08-08",
|
|
945
|
+
status: "development"
|
|
946
|
+
});
|
|
947
|
+
/** A Development-status attribute projection, never a claimed conformant span. */
|
|
948
|
+
export function projectAgentEconomicsReceiptV0ToOpenTelemetryGenAi(value) {
|
|
949
|
+
const receipt = parseAgentEconomicsReceiptV0(value);
|
|
950
|
+
return {
|
|
951
|
+
target: OTEL_GENAI_DEVELOPMENT_PIN,
|
|
952
|
+
receiptId: receipt.id,
|
|
953
|
+
rows: receipt.lines.flatMap((line) => {
|
|
954
|
+
if (line.kind !== "token_usage")
|
|
955
|
+
return [];
|
|
956
|
+
return [{
|
|
957
|
+
sourceLineId: line.id,
|
|
958
|
+
representation: line.granularity === "call" || line.granularity === "invocation"
|
|
959
|
+
? "span_like"
|
|
960
|
+
: "aggregate_record",
|
|
961
|
+
conformantSpan: false,
|
|
962
|
+
attributes: {
|
|
963
|
+
"gen_ai.provider.name": line.provider,
|
|
964
|
+
...(line.requestedModel !== undefined
|
|
965
|
+
? { "gen_ai.request.model": line.requestedModel }
|
|
966
|
+
: {}),
|
|
967
|
+
"gen_ai.usage.input_tokens": line.inputTokens,
|
|
968
|
+
"gen_ai.usage.output_tokens": line.outputTokens
|
|
969
|
+
},
|
|
970
|
+
sourceRecordReferences: line.sourceRecordReferences
|
|
971
|
+
}];
|
|
972
|
+
}),
|
|
973
|
+
gaps: uniqueSortedGaps([
|
|
974
|
+
...receipt.mappingGaps.map((gap) => ({ ...gap })),
|
|
975
|
+
...receipt.lines
|
|
976
|
+
.filter((line) => line.kind === "financial_cost")
|
|
977
|
+
.map((line) => ({ code: "financial_cost_not_projected", lineId: line.id })),
|
|
978
|
+
...receipt.lines
|
|
979
|
+
.filter((line) => line.kind === "token_usage" && line.requestedModel === undefined)
|
|
980
|
+
.map((line) => ({ code: "requested_model_unavailable", lineId: line.id }))
|
|
981
|
+
])
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
export const TOKENOMICS_TRACKING_PIN = Object.freeze({
|
|
985
|
+
standard: "Tokenomics Foundation",
|
|
986
|
+
trackedAsOf: "2026-08-08",
|
|
987
|
+
status: "not_published"
|
|
988
|
+
});
|
|
989
|
+
/** No Tokenomics rows are emitted until a technical specification exists. */
|
|
990
|
+
export function projectAgentEconomicsReceiptV0ToTokenomics(value) {
|
|
991
|
+
const receipt = parseAgentEconomicsReceiptV0(value);
|
|
992
|
+
return {
|
|
993
|
+
target: TOKENOMICS_TRACKING_PIN,
|
|
994
|
+
receiptId: receipt.id,
|
|
995
|
+
rows: [],
|
|
996
|
+
gaps: [{ code: "technical_specification_not_published" }]
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function uniqueSortedGaps(gaps) {
|
|
1000
|
+
const unique = new Map();
|
|
1001
|
+
for (const gap of gaps)
|
|
1002
|
+
unique.set(mappingGapKey(gap), gap);
|
|
1003
|
+
return [...unique.values()].sort((left, right) => compareText(mappingGapKey(left), mappingGapKey(right)));
|
|
1004
|
+
}
|
|
1005
|
+
//# sourceMappingURL=agentEconomicsReceipt.js.map
|