@hue-run/sdk 0.17.0 → 0.18.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/CLI.md +130 -8
- package/EVALUATIONS.md +56 -2
- package/README.md +299 -37
- package/dist/attachment.d.ts +82 -0
- package/dist/attachment.js +225 -0
- package/dist/blobs.d.ts +41 -3
- package/dist/blobs.js +126 -41
- package/dist/cli/import.d.ts +35 -0
- package/dist/cli/import.js +692 -0
- package/dist/cli/langfuse.d.ts +309 -0
- package/dist/cli/langfuse.js +1075 -0
- package/dist/client.d.ts +18 -0
- package/dist/client.js +148 -13
- package/dist/evals/client.d.ts +19 -1
- package/dist/evals/client.js +91 -11
- package/dist/evals/files.d.ts +11 -1
- package/dist/evals/files.js +248 -20
- package/dist/evals/media.d.ts +221 -0
- package/dist/evals/media.js +919 -0
- package/dist/evals/runner.js +130 -106
- package/dist/evals/types.d.ts +49 -0
- package/dist/evals.d.ts +2 -0
- package/dist/evals.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inline-files.d.ts +140 -8
- package/dist/inline-files.js +572 -51
- package/dist/json-edits.d.ts +24 -0
- package/dist/json-edits.js +187 -0
- package/dist/privacy.d.ts +13 -0
- package/dist/privacy.js +84 -17
- package/dist/safety.d.ts +6 -2
- package/dist/safety.js +15 -3
- package/dist/setup/cli.js +7 -3
- package/dist/snapshot.d.ts +1 -1
- package/dist/snapshot.js +123 -29
- package/dist/transport.d.ts +10 -0
- package/dist/transport.js +296 -56
- package/dist/types.d.ts +25 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/snapshot.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createTraceState } from "@opentelemetry/api";
|
|
2
2
|
import { types as utilTypes } from "node:util";
|
|
3
3
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
4
|
-
import { hashInlineFiles, hashNestedInlineFiles, INLINE_FILE_TEXT_PER_RECORD, MAX_INLINE_FILE_TEXT, isMessageKey, mayInlineFiles, } from "./inline-files.js";
|
|
4
|
+
import { hashInlineFiles, hashNestedInlineFiles, INLINE_FILE_TEXT_PER_RECORD, MAX_INLINE_FILE_TEXT, isMediaKey, isMessageKey, literalKeys, mayInlineFiles, readingOf, } from "./inline-files.js";
|
|
5
5
|
import { scrubsToolCredentials, scrubToolCredentials } from "./tool-definitions.js";
|
|
6
6
|
import { MAX_RECORD_NODES } from "./config.js";
|
|
7
7
|
import { truncatedMarker } from "./privacy.js";
|
|
@@ -20,6 +20,25 @@ function cutToUnits(value, units) {
|
|
|
20
20
|
// A slice can keep the whole original string alive; the copy holds only what is kept.
|
|
21
21
|
return Buffer.from(value.slice(0, end), "utf16le").toString("utf16le");
|
|
22
22
|
}
|
|
23
|
+
/** Whether a long string may be a file's inline content: a `data:` URL or base64 text. */
|
|
24
|
+
function mayBeFileContent(text) {
|
|
25
|
+
return text.startsWith("data:") || (text.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(text));
|
|
26
|
+
}
|
|
27
|
+
/** Whether a log record is a correlated GenAI event with a structured body, whose media may be
|
|
28
|
+
* uploaded under its trace. */
|
|
29
|
+
function offloadsLogBody(source) {
|
|
30
|
+
const name = source.eventName ?? source.attributes?.["event.name"];
|
|
31
|
+
const body = source.body;
|
|
32
|
+
const traceId = source.spanContext?.traceId;
|
|
33
|
+
return (typeof name === "string" &&
|
|
34
|
+
name.startsWith("gen_ai.") &&
|
|
35
|
+
body !== null &&
|
|
36
|
+
typeof body === "object" &&
|
|
37
|
+
!utilTypes.isUint8Array(body) &&
|
|
38
|
+
typeof traceId === "string" &&
|
|
39
|
+
/^[0-9a-f]{32}$/.test(traceId) &&
|
|
40
|
+
!/^0+$/.test(traceId));
|
|
41
|
+
}
|
|
23
42
|
/** The record's byte or value budget ran out: a span's events and links past it are left out and
|
|
24
43
|
* counted as dropped, where anything else loses the record. */
|
|
25
44
|
class BudgetExceeded extends RangeError {
|
|
@@ -42,13 +61,25 @@ class Snapshot {
|
|
|
42
61
|
copied = new Map();
|
|
43
62
|
attributesCopied = new Map();
|
|
44
63
|
ownAttributesCopied = new Map();
|
|
64
|
+
/** Message texts as the queue copies them, their media as their digest: a span's own, and
|
|
65
|
+
* nested or a log record's (small binary media kept). */
|
|
45
66
|
hashed = new Map();
|
|
46
67
|
nestedHashed = new Map();
|
|
68
|
+
/** Inspected texts as they would be exported with every file as its digest, by how their
|
|
69
|
+
* attribute is read: what the queue is charged for a value held whole for upload. */
|
|
70
|
+
charged = new Map();
|
|
47
71
|
inspected = 0;
|
|
48
72
|
/** Bytes of values held whole for upload, charged to the held-value budget. */
|
|
49
73
|
held = 0;
|
|
50
74
|
/** Set while the span's own attributes are copied: only they may be held for upload. */
|
|
51
75
|
own = false;
|
|
76
|
+
/** The own attributes holding literal text, while they are copied. */
|
|
77
|
+
literal = new Set();
|
|
78
|
+
/** Set for a log record: only its inspected attributes' media and its body's are held. */
|
|
79
|
+
log = false;
|
|
80
|
+
/** Set while a GenAI log record's structured body is copied: a long string that may be a
|
|
81
|
+
* file's content is held whole for upload. */
|
|
82
|
+
holdingBody = false;
|
|
52
83
|
/** The span's own messages whose inline media shrank to their digest when queued, by
|
|
53
84
|
* attribute: the text queued (cut or not), and how many files it lost. */
|
|
54
85
|
digested = new Map();
|
|
@@ -90,14 +121,25 @@ class Snapshot {
|
|
|
90
121
|
if (!this.hold || !this.own)
|
|
91
122
|
return undefined;
|
|
92
123
|
if (typeof value === "string") {
|
|
124
|
+
// Literal text (`input.value` declared `text/plain`) is no JSON Hue reads files from: it is
|
|
125
|
+
// never inspected for media, but a span's value over the cap is still held whole to upload.
|
|
126
|
+
const literal = this.literal.has(key);
|
|
127
|
+
// A log record's value is never uploaded whole: only the media in its inspected attributes.
|
|
128
|
+
if (this.log && (literal || !isMediaKey(key)))
|
|
129
|
+
return undefined;
|
|
93
130
|
if (value.length > this.valueUnits) {
|
|
131
|
+
if (this.log && (value.length > MAX_INLINE_FILE_TEXT || !mayInlineFiles(key, value)))
|
|
132
|
+
return undefined;
|
|
94
133
|
// The queue must take the cut the value falls back to, as it would have taken it before.
|
|
95
134
|
const fallback = this.valueUnits * 2 + 16;
|
|
96
135
|
if (this.bytes + fallback > this.limit || !this.holds(value.length * 2))
|
|
97
136
|
return undefined;
|
|
98
137
|
this.charge(fallback);
|
|
99
|
-
// Export parses the held message; its inspection allowance is taken here, in order.
|
|
100
|
-
|
|
138
|
+
// Export parses the held message; its inspection allowance is taken here, in order. A
|
|
139
|
+
// log record's export inspects within its own allowance.
|
|
140
|
+
if (!this.log &&
|
|
141
|
+
!literal &&
|
|
142
|
+
value.length <= MAX_INLINE_FILE_TEXT &&
|
|
101
143
|
mayInlineFiles(key, value) &&
|
|
102
144
|
this.inspected + value.length <= INLINE_FILE_TEXT_PER_RECORD) {
|
|
103
145
|
this.inspected += value.length;
|
|
@@ -105,19 +147,19 @@ class Snapshot {
|
|
|
105
147
|
}
|
|
106
148
|
return { value };
|
|
107
149
|
}
|
|
108
|
-
// Within the queued length:
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
// their digest when queued, as before uploads existed.
|
|
112
|
-
if (!
|
|
150
|
+
// Within the queued length: an inspected attribute holding inline media is held whole for
|
|
151
|
+
// export to upload the files, within the held-value budget, and the queue is charged its
|
|
152
|
+
// copy with the files as their digest. Past the budget, a recorded message's files shrink to
|
|
153
|
+
// their digest when queued, as before uploads existed; any other value is queued as it is.
|
|
154
|
+
if (!isMediaKey(key) || literal)
|
|
113
155
|
return undefined;
|
|
114
|
-
const digested = this.inlineFiles(key, value);
|
|
156
|
+
const digested = this.inlineFiles(key, value, true);
|
|
115
157
|
if (digested === value || !this.holds(value.length * 2))
|
|
116
158
|
return undefined;
|
|
117
159
|
this.copy(digested, 1, "value", true);
|
|
118
160
|
return { value };
|
|
119
161
|
}
|
|
120
|
-
if (!utilTypes.isUint8Array(value))
|
|
162
|
+
if (this.log || !utilTypes.isUint8Array(value))
|
|
121
163
|
return undefined;
|
|
122
164
|
const length = typedArrayByteLength.call(value);
|
|
123
165
|
// Bytes over Hue's upload limit could only be exported cut: not copied whole for that.
|
|
@@ -198,39 +240,58 @@ class Snapshot {
|
|
|
198
240
|
/** The span's own attribute map, whose values over the cap may be held whole for upload. */
|
|
199
241
|
ownAttributes(value) {
|
|
200
242
|
this.own = true;
|
|
243
|
+
this.literal = literalKeys(value);
|
|
201
244
|
try {
|
|
202
245
|
return this.attributes(value);
|
|
203
246
|
}
|
|
204
247
|
finally {
|
|
205
248
|
this.own = false;
|
|
249
|
+
this.literal = new Set();
|
|
206
250
|
}
|
|
207
251
|
}
|
|
208
|
-
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
|
|
213
|
-
|
|
252
|
+
/**
|
|
253
|
+
* An attribute with its inline media hashed. As queued (`charge` false), only a recorded message
|
|
254
|
+
* shrinks: a span's own with every binary file and text file over the inline limit as its
|
|
255
|
+
* digest, a nested one's or a log record's keeping small binary media. With `charge`, any
|
|
256
|
+
* inspected attribute as export would place it with every file as its digest, what the queue is
|
|
257
|
+
* charged for a value held whole for upload. Own, nested and charged copies cache separately. A
|
|
258
|
+
* record inspects at most {@link INLINE_FILE_TEXT_PER_RECORD} code units in all; beyond that,
|
|
259
|
+
* values are charged as recorded. Nothing is inspected in metadata-only mode.
|
|
260
|
+
*/
|
|
261
|
+
inlineFiles(key, value, charge = false) {
|
|
262
|
+
if (!this.captureContent ||
|
|
263
|
+
typeof value !== "string" ||
|
|
214
264
|
value.length > MAX_INLINE_FILE_TEXT ||
|
|
215
|
-
!mayInlineFiles(key, value)
|
|
265
|
+
!mayInlineFiles(key, value) ||
|
|
266
|
+
(!charge && !isMessageKey(key)))
|
|
216
267
|
return value;
|
|
217
|
-
const
|
|
268
|
+
const own = this.own && !this.log;
|
|
269
|
+
// A span's own message is queued as it is charged: one copy serves both.
|
|
270
|
+
const shared = own && isMessageKey(key);
|
|
271
|
+
let hashed;
|
|
272
|
+
if (charge && !shared) {
|
|
273
|
+
const reading = readingOf(key);
|
|
274
|
+
hashed = this.charged.get(reading) ?? new Map();
|
|
275
|
+
this.charged.set(reading, hashed);
|
|
276
|
+
}
|
|
277
|
+
else
|
|
278
|
+
hashed = own ? this.hashed : this.nestedHashed;
|
|
218
279
|
if (hashed.has(value)) {
|
|
219
|
-
if (
|
|
280
|
+
if (own)
|
|
220
281
|
this.inspectedKeys.add(key);
|
|
221
282
|
return hashed.get(value);
|
|
222
283
|
}
|
|
223
284
|
if (this.inspected + value.length > INLINE_FILE_TEXT_PER_RECORD)
|
|
224
285
|
return value;
|
|
225
286
|
this.inspected += value.length;
|
|
226
|
-
if (
|
|
287
|
+
if (own)
|
|
227
288
|
this.inspectedKeys.add(key);
|
|
228
289
|
const count = { files: 0 };
|
|
229
|
-
const result =
|
|
230
|
-
? hashInlineFiles(key, value, count, this.memo, this.traceId)
|
|
290
|
+
const result = charge || own
|
|
291
|
+
? hashInlineFiles(key, value, count, this.memo, this.traceId, true)
|
|
231
292
|
: hashNestedInlineFiles(key, value);
|
|
232
293
|
hashed.set(value, result);
|
|
233
|
-
if (
|
|
294
|
+
if (shared && count.files)
|
|
234
295
|
this.hashedFiles.set(value, count.files);
|
|
235
296
|
return result;
|
|
236
297
|
}
|
|
@@ -276,6 +337,19 @@ class Snapshot {
|
|
|
276
337
|
value(value, depth = 1) {
|
|
277
338
|
return this.copy(value, depth, "value", true);
|
|
278
339
|
}
|
|
340
|
+
/** A GenAI log record's structured body: strings are cut to the queued value length as in
|
|
341
|
+
* {@link value}, except that a longer string that may be a file's content (a `data:` URL or
|
|
342
|
+
* base64) within the inspection limit is held whole for export to upload, within the
|
|
343
|
+
* held-value budget, the queue charged what its cut would have cost. */
|
|
344
|
+
heldBody(value) {
|
|
345
|
+
this.holdingBody = this.hold !== undefined;
|
|
346
|
+
try {
|
|
347
|
+
return this.copy(value, 1, "value", true);
|
|
348
|
+
}
|
|
349
|
+
finally {
|
|
350
|
+
this.holdingBody = false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
279
353
|
copy(value, depth = 0, shape = "value", content = false) {
|
|
280
354
|
if (++this.nodes > MAX_RECORD_NODES)
|
|
281
355
|
throw new BudgetExceeded("Telemetry complexity limit exceeded");
|
|
@@ -285,6 +359,13 @@ class Snapshot {
|
|
|
285
359
|
if (typeof value === "string") {
|
|
286
360
|
let text = value;
|
|
287
361
|
if (content && text.length > this.valueUnits) {
|
|
362
|
+
if (this.holdingBody && text.length <= MAX_INLINE_FILE_TEXT && mayBeFileContent(text)) {
|
|
363
|
+
const fallback = this.valueUnits * 2 + 16;
|
|
364
|
+
if (this.bytes + fallback <= this.limit && this.holds(text.length * 2)) {
|
|
365
|
+
this.charge(fallback);
|
|
366
|
+
return text;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
288
369
|
text = cutToUnits(text, this.valueUnits);
|
|
289
370
|
this.cut.add(text);
|
|
290
371
|
}
|
|
@@ -361,7 +442,11 @@ class Snapshot {
|
|
|
361
442
|
copy[key] = copied;
|
|
362
443
|
// A span's own message whose inline media shrank to their digest is named as it
|
|
363
444
|
// was queued, cut or not, so export reports each file as not uploaded.
|
|
364
|
-
const files = this.own &&
|
|
445
|
+
const files = this.own &&
|
|
446
|
+
!this.log &&
|
|
447
|
+
shape === "attributes" &&
|
|
448
|
+
typeof source === "string" &&
|
|
449
|
+
isMessageKey(key)
|
|
365
450
|
? this.hashedFiles.get(source)
|
|
366
451
|
: undefined;
|
|
367
452
|
if (files && typeof copied === "string")
|
|
@@ -462,8 +547,13 @@ export function snapshotSpan(source, limit, captureContent = true, valueUnits =
|
|
|
462
547
|
inspected: snapshot.inspectedKeys,
|
|
463
548
|
};
|
|
464
549
|
}
|
|
465
|
-
export function snapshotLog(source, limit, captureContent = true, valueUnits = Infinity) {
|
|
466
|
-
|
|
550
|
+
export function snapshotLog(source, limit, captureContent = true, valueUnits = Infinity, hold, memo) {
|
|
551
|
+
// A log record's media upload under its own trace: an uncorrelated record holds none.
|
|
552
|
+
const traceId = source.spanContext?.traceId;
|
|
553
|
+
const correlated = typeof traceId === "string" && /^[0-9a-f]{32}$/.test(traceId) && !/^0+$/.test(traceId);
|
|
554
|
+
const holding = correlated ? hold : undefined;
|
|
555
|
+
const snapshot = new Snapshot(limit, captureContent, valueUnits, holding, memo, traceId ?? "");
|
|
556
|
+
snapshot.log = true;
|
|
467
557
|
// Only our batching processor sees this copy. Its writer methods deliberately
|
|
468
558
|
// cannot mutate the admitted snapshot or invalidate its charged byte count.
|
|
469
559
|
const record = {
|
|
@@ -476,8 +566,12 @@ export function snapshotLog(source, limit, captureContent = true, valueUnits = I
|
|
|
476
566
|
instrumentationScope: source.instrumentationScope,
|
|
477
567
|
droppedAttributesCount: source.droppedAttributesCount,
|
|
478
568
|
}),
|
|
479
|
-
body:
|
|
480
|
-
|
|
569
|
+
body: holding && captureContent && offloadsLogBody(source)
|
|
570
|
+
? snapshot.heldBody(source.body)
|
|
571
|
+
: snapshot.value(source.body),
|
|
572
|
+
attributes: holding
|
|
573
|
+
? snapshot.ownAttributes(source.attributes)
|
|
574
|
+
: snapshot.attributes(source.attributes),
|
|
481
575
|
spanContext: snapshot.context(source.spanContext),
|
|
482
576
|
resource: snapshot.resource(source.resource),
|
|
483
577
|
setAttribute() {
|
|
@@ -504,7 +598,7 @@ export function snapshotLog(source, limit, captureContent = true, valueUnits = I
|
|
|
504
598
|
bytes: snapshot.bytes,
|
|
505
599
|
unresolvedResource: snapshot.unresolvedResource,
|
|
506
600
|
cut: snapshot.cut,
|
|
507
|
-
held:
|
|
601
|
+
held: snapshot.held,
|
|
508
602
|
digested: new Map(),
|
|
509
603
|
inspected: new Set(),
|
|
510
604
|
};
|
package/dist/transport.d.ts
CHANGED
|
@@ -57,6 +57,9 @@ export declare class HueTransport {
|
|
|
57
57
|
* over the request limit), reported on the trace's root span as `hue.sdk.dropped_records` when
|
|
58
58
|
* the root is exported, so Hue reads the trace as incomplete by that many records. */
|
|
59
59
|
private droppedByTrace;
|
|
60
|
+
/** Whether a trace's root carries `hue.sdk.dropped_records`; off for a replay of recorded
|
|
61
|
+
* spans, which must send the same root on every run. */
|
|
62
|
+
private marksDroppedRecords;
|
|
60
63
|
private instrumentationFailures;
|
|
61
64
|
private diagnosticPending;
|
|
62
65
|
private lastDiagnosticAt;
|
|
@@ -87,6 +90,8 @@ export declare class HueTransport {
|
|
|
87
90
|
private held;
|
|
88
91
|
private mediaMemo;
|
|
89
92
|
private media;
|
|
93
|
+
private logParts;
|
|
94
|
+
private logMedia;
|
|
90
95
|
private mediaOwners;
|
|
91
96
|
private mediaTimer?;
|
|
92
97
|
private mediaFinalizer;
|
|
@@ -104,6 +109,11 @@ export declare class HueTransport {
|
|
|
104
109
|
private announceLiveSpans;
|
|
105
110
|
private stopLiveTimer;
|
|
106
111
|
private stopLiveSpans;
|
|
112
|
+
/** Holds the staged attachments of a log record being admitted with the queued record, within
|
|
113
|
+
* the held-value budget; those the budget refuses are counted as not uploaded. */
|
|
114
|
+
private admitLogParts;
|
|
115
|
+
/** Registers a span whose staged files are released if it is never exported. */
|
|
116
|
+
private ownMedia;
|
|
107
117
|
private mediaIdentity;
|
|
108
118
|
private releaseMedia;
|
|
109
119
|
private protectMedia;
|