@opengeni/runtime 0.11.0 → 0.13.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.
@@ -1,73 +1,517 @@
1
1
  type UsageDetails = Record<string, unknown> | Array<Record<string, unknown>>;
2
2
 
3
+ type RequestUsageEntry = {
4
+ inputTokens?: unknown;
5
+ input_tokens?: unknown;
6
+ outputTokens?: unknown;
7
+ output_tokens?: unknown;
8
+ totalTokens?: unknown;
9
+ total_tokens?: unknown;
10
+ inputTokensDetails?: UsageDetails | undefined;
11
+ input_tokens_details?: UsageDetails | undefined;
12
+ outputTokensDetails?: UsageDetails | undefined;
13
+ output_tokens_details?: UsageDetails | undefined;
14
+ };
15
+
16
+ export type ModelCallUsageInput = {
17
+ inputTokens?: unknown;
18
+ outputTokens?: unknown;
19
+ totalTokens?: unknown;
20
+ inputTokensDetails?: UsageDetails | undefined;
21
+ outputTokensDetails?: UsageDetails | undefined;
22
+ requestUsageEntries?: RequestUsageEntry[] | undefined;
23
+ };
24
+
25
+ /**
26
+ * A single provider usage frame above one billion tokens is outside every
27
+ * supported OpenGeni model contract. Keeping the ceiling explicit also prevents
28
+ * malformed-but-finite provider values from corrupting durable accounting or a
29
+ * process-lifetime Prometheus counter.
30
+ */
31
+ export const MAX_MODEL_USAGE_TOKEN_COUNT = 1_000_000_000;
32
+
3
33
  export type ModelCallUsageTelemetry = {
4
34
  inputTokens: number | null;
5
35
  outputTokens: number | null;
6
36
  cachedTokens: number | null;
37
+ cacheWriteTokens: number | null;
7
38
  reasoningTokens: number | null;
8
39
  };
9
40
 
10
- export function modelCallUsageTelemetry(
11
- usage:
12
- | {
13
- inputTokens?: unknown;
14
- outputTokens?: unknown;
15
- inputTokensDetails?: UsageDetails | undefined;
16
- outputTokensDetails?: UsageDetails | undefined;
17
- }
18
- | null
19
- | undefined,
20
- ): ModelCallUsageTelemetry {
41
+ export type ModelCallUsageNormalization = {
42
+ telemetry: ModelCallUsageTelemetry;
43
+ totalTokens: number | null;
44
+ /** Bounded field paths only; raw provider values are never retained. */
45
+ rejectedFields: string[];
46
+ };
47
+
48
+ export function modelUsageTokenCountOrNull(value: unknown): number | null {
49
+ return typeof value === "number" &&
50
+ Number.isSafeInteger(value) &&
51
+ value >= 0 &&
52
+ value <= MAX_MODEL_USAGE_TOKEN_COUNT
53
+ ? value
54
+ : null;
55
+ }
56
+
57
+ export function normalizeModelCallUsage(
58
+ usage: ModelCallUsageInput | null | undefined,
59
+ ): ModelCallUsageNormalization {
60
+ const rejectedFields = new Set<string>();
61
+ if (!usage) {
62
+ return {
63
+ telemetry: {
64
+ inputTokens: null,
65
+ outputTokens: null,
66
+ cachedTokens: null,
67
+ cacheWriteTokens: null,
68
+ reasoningTokens: null,
69
+ },
70
+ totalTokens: null,
71
+ rejectedFields: [],
72
+ };
73
+ }
74
+
75
+ const requestEntries = requestUsageEntries(usage.requestUsageEntries, rejectedFields);
76
+ const topLevelInputTokens = reportedTokenCount(usage.inputTokens, "inputTokens", rejectedFields);
77
+ const topLevelOutputTokens = reportedTokenCount(
78
+ usage.outputTokens,
79
+ "outputTokens",
80
+ rejectedFields,
81
+ );
82
+ const topLevelTotalTokens = reportedTokenCount(usage.totalTokens, "totalTokens", rejectedFields);
83
+ const requestInputTokens = completeRequestTokenAggregate(
84
+ requestEntries,
85
+ ["inputTokens", "input_tokens"],
86
+ "inputTokens.requestUsageEntries",
87
+ rejectedFields,
88
+ );
89
+ const requestOutputTokens = completeRequestTokenAggregate(
90
+ requestEntries,
91
+ ["outputTokens", "output_tokens"],
92
+ "outputTokens.requestUsageEntries",
93
+ rejectedFields,
94
+ );
95
+ let requestTotalTokens = completeRequestTokenAggregate(
96
+ requestEntries,
97
+ ["totalTokens", "total_tokens"],
98
+ "totalTokens.requestUsageEntries",
99
+ rejectedFields,
100
+ );
101
+ if (rejectInconsistentRequestTotals(requestEntries, rejectedFields)) {
102
+ requestTotalTokens = { ...requestTotalTokens, status: "invalid", value: null };
103
+ }
104
+
105
+ // Complete per-request aggregates are the installed SDK's request-level
106
+ // truth. A conflicting top-level field is diagnostic only; it must never
107
+ // override the request sum with a smaller billable/context value. Invalid
108
+ // complete request aggregates fail closed rather than falling back to a
109
+ // potentially undercounting top-level number.
110
+ const inputTokens = canonicalTokenDimension({
111
+ topLevel: topLevelInputTokens,
112
+ topLevelReported: isReported(usage.inputTokens),
113
+ topLevelPath: "inputTokens",
114
+ requestAggregate: requestInputTokens,
115
+ rejectedFields,
116
+ });
117
+ const outputTokens = canonicalTokenDimension({
118
+ topLevel: topLevelOutputTokens,
119
+ topLevelReported: isReported(usage.outputTokens),
120
+ topLevelPath: "outputTokens",
121
+ requestAggregate: requestOutputTokens,
122
+ rejectedFields,
123
+ });
124
+ const totalTokens = canonicalTotalTokens({
125
+ inputTokens,
126
+ outputTokens,
127
+ topLevel: topLevelTotalTokens,
128
+ topLevelReported: isReported(usage.totalTokens),
129
+ requestAggregate: requestTotalTokens,
130
+ rejectedFields,
131
+ });
132
+
133
+ const inputDetailSource = preferredDetailSource(
134
+ usage.inputTokensDetails,
135
+ "inputTokensDetails",
136
+ requestEntries,
137
+ ["inputTokensDetails", "input_tokens_details"],
138
+ );
139
+ const outputDetailSource = preferredDetailSource(
140
+ usage.outputTokensDetails,
141
+ "outputTokensDetails",
142
+ requestEntries,
143
+ ["outputTokensDetails", "output_tokens_details"],
144
+ );
145
+
21
146
  return {
22
- inputTokens: finiteNumberOrNull(usage?.inputTokens),
23
- outputTokens: finiteNumberOrNull(usage?.outputTokens),
24
- cachedTokens: usage
25
- ? firstPositiveDetailNumber(usage.inputTokensDetails, [
26
- "cached_tokens",
27
- "cachedInputTokens",
28
- "cached_input_tokens",
29
- ])
30
- : null,
31
- reasoningTokens: usage
32
- ? firstPositiveDetailNumber(usage.outputTokensDetails, [
33
- "reasoning_tokens",
34
- "reasoningTokens",
35
- "reasoning_output_tokens",
36
- ])
37
- : null,
147
+ telemetry: {
148
+ inputTokens,
149
+ outputTokens,
150
+ cachedTokens: aggregateDetailTokenCount(
151
+ inputDetailSource,
152
+ ["cached_tokens", "cachedInputTokens", "cached_input_tokens"],
153
+ "cachedTokens",
154
+ rejectedFields,
155
+ ),
156
+ cacheWriteTokens: aggregateDetailTokenCount(
157
+ inputDetailSource,
158
+ ["cache_write_tokens", "cacheWriteTokens"],
159
+ "cacheWriteTokens",
160
+ rejectedFields,
161
+ ),
162
+ reasoningTokens: aggregateDetailTokenCount(
163
+ outputDetailSource,
164
+ ["reasoning_tokens", "reasoningTokens", "reasoning_output_tokens"],
165
+ "reasoningTokens",
166
+ rejectedFields,
167
+ ),
168
+ },
169
+ totalTokens,
170
+ rejectedFields: [...rejectedFields].slice(0, 32),
38
171
  };
39
172
  }
40
173
 
41
- function firstPositiveDetailNumber(
42
- details: UsageDetails | undefined,
43
- keys: string[],
44
- ): number | null {
45
- if (!details) {
174
+ export function modelCallUsageTelemetry(
175
+ usage: ModelCallUsageInput | null | undefined,
176
+ ): ModelCallUsageTelemetry {
177
+ return normalizeModelCallUsage(usage).telemetry;
178
+ }
179
+
180
+ function requestUsageEntries(
181
+ value: RequestUsageEntry[] | undefined,
182
+ rejectedFields: Set<string>,
183
+ ): RequestUsageEntry[] {
184
+ if (value === undefined) {
185
+ return [];
186
+ }
187
+ if (!Array.isArray(value)) {
188
+ rejectField(rejectedFields, "requestUsageEntries");
189
+ return [];
190
+ }
191
+ const entries: RequestUsageEntry[] = [];
192
+ for (const [index, entry] of value.entries()) {
193
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
194
+ rejectField(rejectedFields, `requestUsageEntries[${index}]`);
195
+ // Preserve the request slot as unknown. Dropping it would let fallback
196
+ // aggregation silently undercount only the well-formed requests.
197
+ entries.push({});
198
+ continue;
199
+ }
200
+ entries.push(entry);
201
+ }
202
+ return entries;
203
+ }
204
+
205
+ type RequestTokenAggregate = {
206
+ status: "absent" | "incomplete" | "invalid" | "complete";
207
+ value: number | null;
208
+ };
209
+
210
+ function completeRequestTokenAggregate(
211
+ requestEntries: RequestUsageEntry[],
212
+ requestKeys: string[],
213
+ aggregatePath: string,
214
+ rejectedFields: Set<string>,
215
+ ): RequestTokenAggregate {
216
+ if (requestEntries.length === 0) {
217
+ return { status: "absent", value: null };
218
+ }
219
+ const values: Array<number | null> = [];
220
+ let incomplete = false;
221
+ let invalid = false;
222
+ for (const [index, entry] of requestEntries.entries()) {
223
+ const selected = firstPresentEntry(entry as Record<string, unknown>, requestKeys);
224
+ if (!selected || selected.value === null) {
225
+ incomplete = true;
226
+ continue;
227
+ }
228
+ const parsed = reportedTokenCount(
229
+ selected.value,
230
+ `requestUsageEntries[${index}].${selected.key}`,
231
+ rejectedFields,
232
+ );
233
+ if (parsed === null) invalid = true;
234
+ values.push(parsed);
235
+ }
236
+ if (invalid) {
237
+ return { status: "invalid", value: null };
238
+ }
239
+ if (incomplete) {
240
+ return { status: "incomplete", value: null };
241
+ }
242
+ const value = boundedTokenSum(values, aggregatePath, rejectedFields);
243
+ return value === null ? { status: "invalid", value: null } : { status: "complete", value };
244
+ }
245
+
246
+ function canonicalTokenDimension(input: {
247
+ topLevel: number | null;
248
+ topLevelReported: boolean;
249
+ topLevelPath: string;
250
+ requestAggregate: RequestTokenAggregate;
251
+ rejectedFields: Set<string>;
252
+ }): number | null {
253
+ if (input.requestAggregate.status === "complete") {
254
+ if (
255
+ input.topLevelReported &&
256
+ input.topLevel !== null &&
257
+ input.topLevel !== input.requestAggregate.value
258
+ ) {
259
+ rejectField(input.rejectedFields, input.topLevelPath);
260
+ }
261
+ return input.requestAggregate.value;
262
+ }
263
+ if (input.requestAggregate.status === "invalid") {
264
+ if (input.topLevelReported) {
265
+ rejectField(input.rejectedFields, input.topLevelPath);
266
+ }
46
267
  return null;
47
268
  }
48
- // A reported 0 is REAL DATA ("the provider cached/reasoned nothing"), not
49
- // absence — the old >0-only filter recorded wire `cached_tokens: 0` as null,
50
- // making "0% cached" indistinguishable from "no telemetry" (which is exactly
51
- // how 10k+ genuinely-uncached Azure calls masqueraded as a telemetry gap).
52
- // Prefer the first positive value across entries (multi-entry detail arrays
53
- // may carry zero placeholders next to the real number), but if every entry
54
- // says 0, report 0.
55
- const entries = Array.isArray(details) ? details : [details];
56
- let sawZero = false;
57
- for (const entry of entries) {
58
- for (const key of keys) {
59
- const value = finiteNumberOrNull(entry[key]);
60
- if (value !== null && value > 0) {
61
- return value;
269
+ return input.topLevel;
270
+ }
271
+
272
+ function canonicalTotalTokens(input: {
273
+ inputTokens: number | null;
274
+ outputTokens: number | null;
275
+ topLevel: number | null;
276
+ topLevelReported: boolean;
277
+ requestAggregate: RequestTokenAggregate;
278
+ rejectedFields: Set<string>;
279
+ }): number | null {
280
+ if (input.inputTokens !== null && input.outputTokens !== null) {
281
+ const derived = boundedTokenSum(
282
+ [input.inputTokens, input.outputTokens],
283
+ "totalTokens.aggregate",
284
+ input.rejectedFields,
285
+ );
286
+ if (derived === null) {
287
+ // Both canonical components are known and exceed the supported aggregate
288
+ // ceiling. Any bounded reported total necessarily undercounts them.
289
+ if (input.topLevelReported) rejectField(input.rejectedFields, "totalTokens");
290
+ if (input.requestAggregate.status === "complete") {
291
+ rejectField(input.rejectedFields, "totalTokens.requestUsageEntries");
62
292
  }
63
- if (value === 0) {
64
- sawZero = true;
293
+ return null;
294
+ }
295
+ if (input.topLevelReported && input.topLevel !== null && input.topLevel !== derived) {
296
+ rejectField(input.rejectedFields, "totalTokens");
297
+ }
298
+ if (input.requestAggregate.status === "complete" && input.requestAggregate.value !== derived) {
299
+ rejectField(input.rejectedFields, "totalTokens.requestUsageEntries");
300
+ }
301
+ return derived;
302
+ }
303
+
304
+ if (input.requestAggregate.status === "complete") {
305
+ const requestTotal = input.requestAggregate.value;
306
+ if (
307
+ requestTotal === null ||
308
+ !totalCoversKnownComponents(requestTotal, input.inputTokens, input.outputTokens)
309
+ ) {
310
+ rejectField(input.rejectedFields, "totalTokens.requestUsageEntries");
311
+ if (input.topLevelReported) rejectField(input.rejectedFields, "totalTokens");
312
+ return null;
313
+ }
314
+ if (input.topLevelReported && input.topLevel !== null && input.topLevel !== requestTotal) {
315
+ rejectField(input.rejectedFields, "totalTokens");
316
+ }
317
+ return requestTotal;
318
+ }
319
+ if (input.requestAggregate.status === "invalid") {
320
+ if (input.topLevelReported) rejectField(input.rejectedFields, "totalTokens");
321
+ return null;
322
+ }
323
+ if (
324
+ input.topLevel !== null &&
325
+ !totalCoversKnownComponents(input.topLevel, input.inputTokens, input.outputTokens)
326
+ ) {
327
+ rejectField(input.rejectedFields, "totalTokens");
328
+ return null;
329
+ }
330
+ return input.topLevel;
331
+ }
332
+
333
+ function totalCoversKnownComponents(
334
+ total: number,
335
+ inputTokens: number | null,
336
+ outputTokens: number | null,
337
+ ): boolean {
338
+ return (
339
+ (inputTokens === null || total >= inputTokens) &&
340
+ (outputTokens === null || total >= outputTokens)
341
+ );
342
+ }
343
+
344
+ function rejectInconsistentRequestTotals(
345
+ requestEntries: RequestUsageEntry[],
346
+ rejectedFields: Set<string>,
347
+ ): boolean {
348
+ let inconsistent = false;
349
+ for (const [index, entry] of requestEntries.entries()) {
350
+ const record = entry as Record<string, unknown>;
351
+ const selectedInput = firstPresentEntry(record, ["inputTokens", "input_tokens"]);
352
+ const selectedOutput = firstPresentEntry(record, ["outputTokens", "output_tokens"]);
353
+ const selectedTotal = firstPresentEntry(record, ["totalTokens", "total_tokens"]);
354
+ if (!selectedInput || !selectedOutput || !selectedTotal) continue;
355
+ const inputTokens = modelUsageTokenCountOrNull(selectedInput.value);
356
+ const outputTokens = modelUsageTokenCountOrNull(selectedOutput.value);
357
+ const totalTokens = modelUsageTokenCountOrNull(selectedTotal.value);
358
+ if (inputTokens === null || outputTokens === null || totalTokens === null) continue;
359
+ const derived =
360
+ inputTokens <= MAX_MODEL_USAGE_TOKEN_COUNT - outputTokens ? inputTokens + outputTokens : null;
361
+ if (derived === null || totalTokens !== derived) {
362
+ rejectField(rejectedFields, `requestUsageEntries[${index}].${selectedTotal.key}`);
363
+ inconsistent = true;
364
+ }
365
+ }
366
+ return inconsistent;
367
+ }
368
+
369
+ function isReported(value: unknown): boolean {
370
+ return value !== undefined && value !== null;
371
+ }
372
+
373
+ function preferredDetailSource(
374
+ aggregateDetails: UsageDetails | undefined,
375
+ aggregatePath: string,
376
+ requestEntries: RequestUsageEntry[],
377
+ requestKeys: string[],
378
+ ): Array<{ details: unknown; path: string; missing?: boolean }> {
379
+ const requestDetails: Array<{ details: unknown; path: string; missing?: boolean }> = [];
380
+ let sawRequestDetails = false;
381
+ for (const [index, entry] of requestEntries.entries()) {
382
+ const selected = firstPresentEntry(entry as Record<string, unknown>, requestKeys);
383
+ if (!selected) {
384
+ requestDetails.push({
385
+ details: undefined,
386
+ path: `requestUsageEntries[${index}].${requestKeys[0]}`,
387
+ missing: true,
388
+ });
389
+ continue;
390
+ }
391
+ sawRequestDetails = true;
392
+ requestDetails.push({
393
+ details: selected.value,
394
+ path: `requestUsageEntries[${index}].${selected.key}`,
395
+ });
396
+ }
397
+ // The installed Agents SDK explicitly carries per-request entries. Prefer
398
+ // them when they include detail records; otherwise fall back to the aggregate
399
+ // detail arrays that Usage.add() appends in request order.
400
+ return sawRequestDetails
401
+ ? requestDetails
402
+ : aggregateDetails === undefined
403
+ ? []
404
+ : [{ details: aggregateDetails, path: aggregatePath }];
405
+ }
406
+
407
+ function aggregateDetailTokenCount(
408
+ sources: Array<{ details: unknown; path: string; missing?: boolean }>,
409
+ keys: string[],
410
+ field: string,
411
+ rejectedFields: Set<string>,
412
+ ): number | null {
413
+ const values: Array<number | null> = [];
414
+ let sawReportedValue = false;
415
+ let sawMissingValue = false;
416
+ for (const source of sources) {
417
+ if (source.missing) {
418
+ sawMissingValue = true;
419
+ continue;
420
+ }
421
+ const details = detailRecords(source.details, source.path, rejectedFields);
422
+ if (!details.complete) {
423
+ sawMissingValue = true;
424
+ }
425
+ for (const detail of details.records) {
426
+ const selected = firstPresentEntry(detail.record, keys);
427
+ if (!selected) {
428
+ sawMissingValue = true;
429
+ continue;
65
430
  }
431
+ sawReportedValue = true;
432
+ values.push(
433
+ reportedTokenCount(selected.value, `${detail.path}.${selected.key}`, rejectedFields),
434
+ );
66
435
  }
67
436
  }
68
- return sawZero ? 0 : null;
437
+ return sawReportedValue && !sawMissingValue
438
+ ? boundedTokenSum(values, `${field}.aggregate`, rejectedFields)
439
+ : null;
69
440
  }
70
441
 
71
- function finiteNumberOrNull(value: unknown): number | null {
72
- return typeof value === "number" && Number.isFinite(value) ? value : null;
442
+ function detailRecords(
443
+ details: unknown,
444
+ path: string,
445
+ rejectedFields: Set<string>,
446
+ ): {
447
+ records: Array<{ record: Record<string, unknown>; path: string }>;
448
+ complete: boolean;
449
+ } {
450
+ const entries = Array.isArray(details) ? details : [details];
451
+ const records: Array<{ record: Record<string, unknown>; path: string }> = [];
452
+ let complete = true;
453
+ for (const [index, entry] of entries.entries()) {
454
+ const entryPath = Array.isArray(details) ? `${path}[${index}]` : path;
455
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
456
+ rejectField(rejectedFields, entryPath);
457
+ complete = false;
458
+ continue;
459
+ }
460
+ records.push({ record: entry as Record<string, unknown>, path: entryPath });
461
+ }
462
+ return { records, complete };
463
+ }
464
+
465
+ function firstPresentEntry(
466
+ record: Record<string, unknown>,
467
+ keys: string[],
468
+ ): { key: string; value: unknown } | null {
469
+ for (const key of keys) {
470
+ if (Object.prototype.hasOwnProperty.call(record, key) && record[key] !== undefined) {
471
+ // One request can expose the same value under provider and SDK aliases.
472
+ // Pick the first canonical alias instead of adding aliases together.
473
+ return { key, value: record[key] };
474
+ }
475
+ }
476
+ return null;
477
+ }
478
+
479
+ function reportedTokenCount(
480
+ value: unknown,
481
+ field: string,
482
+ rejectedFields: Set<string>,
483
+ ): number | null {
484
+ if (value === undefined || value === null) {
485
+ return null;
486
+ }
487
+ const normalized = modelUsageTokenCountOrNull(value);
488
+ if (normalized === null) {
489
+ rejectField(rejectedFields, field);
490
+ }
491
+ return normalized;
492
+ }
493
+
494
+ function boundedTokenSum(
495
+ values: Array<number | null>,
496
+ field: string,
497
+ rejectedFields: Set<string>,
498
+ ): number | null {
499
+ if (values.length === 0 || values.some((value) => value === null)) {
500
+ return null;
501
+ }
502
+ let total = 0;
503
+ for (const value of values as number[]) {
504
+ if (value > MAX_MODEL_USAGE_TOKEN_COUNT - total) {
505
+ rejectField(rejectedFields, field);
506
+ return null;
507
+ }
508
+ total += value;
509
+ }
510
+ return total;
511
+ }
512
+
513
+ function rejectField(rejectedFields: Set<string>, field: string): void {
514
+ if (rejectedFields.size < 32) {
515
+ rejectedFields.add(field);
516
+ }
73
517
  }