@hue-run/sdk 0.15.2 → 0.17.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/CLI.md +30 -21
- package/EVALUATIONS.md +1 -1
- package/README.md +71 -49
- package/dist/blobs.d.ts +35 -17
- package/dist/blobs.js +141 -75
- package/dist/cli/env-file.d.ts +2 -0
- package/dist/cli/env-file.js +2 -0
- package/dist/cli/eval-direct.d.ts +3 -2
- package/dist/cli/eval-direct.js +49 -6
- package/dist/cli/eval.js +109 -22
- package/dist/cli/login.js +2 -3
- package/dist/client.d.ts +3 -1
- package/dist/client.js +70 -20
- package/dist/evals/attempt.d.ts +1 -1
- package/dist/evals/attempt.js +1 -1
- package/dist/evals/checkpoint.d.ts +6 -2
- package/dist/evals/checkpoint.js +75 -12
- package/dist/evals/files.js +12 -0
- package/dist/evals/runner.d.ts +9 -0
- package/dist/evals/runner.js +63 -10
- package/dist/evals/scorer-publication.js +1 -1
- package/dist/evals/simulation.js +4 -2
- package/dist/inline-files.d.ts +38 -29
- package/dist/inline-files.js +288 -83
- package/dist/privacy.d.ts +1 -1
- package/dist/privacy.js +4 -10
- package/dist/safety.d.ts +1 -0
- package/dist/safety.js +7 -1
- package/dist/snapshot.d.ts +6 -3
- package/dist/snapshot.js +70 -25
- package/dist/transport.d.ts +11 -0
- package/dist/transport.js +296 -20
- package/dist/types.d.ts +7 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -10
package/dist/inline-files.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { MAX_BODY_BYTES } from "./config.js";
|
|
3
|
-
/**
|
|
3
|
+
/** Text media above this decoded byte limit is offloaded; binary media always uses a digest. */
|
|
4
4
|
export const INLINE_FILE_LIMIT = 64 * 1024;
|
|
5
|
-
/** The longest message text, in UTF-
|
|
5
|
+
/** The longest message text, in UTF-16 code units, inspected for inline files. */
|
|
6
6
|
export const MAX_INLINE_FILE_TEXT = 8 * MAX_BODY_BYTES;
|
|
7
7
|
/** Message text one record inspects for inline files in all, in UTF-16 code units. */
|
|
8
8
|
export const INLINE_FILE_TEXT_PER_RECORD = 2 * MAX_INLINE_FILE_TEXT;
|
|
9
|
-
/**
|
|
9
|
+
/** The span message attributes whose JSON may contain inline media. */
|
|
10
10
|
const messageKeys = new Set([
|
|
11
11
|
"gen_ai.input.messages",
|
|
12
12
|
"gen_ai.output.messages",
|
|
@@ -69,76 +69,200 @@ export function inlineFileBytes(content) {
|
|
|
69
69
|
...(mediaType ? { urlMediaType: mediaType } : {}),
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
const audioTypes = {
|
|
73
|
+
wav: "audio/wav",
|
|
74
|
+
mp3: "audio/mpeg",
|
|
75
|
+
flac: "audio/flac",
|
|
76
|
+
ogg: "audio/ogg",
|
|
77
|
+
opus: "audio/opus",
|
|
78
|
+
aac: "audio/aac",
|
|
79
|
+
m4a: "audio/mp4",
|
|
80
|
+
webm: "audio/webm",
|
|
81
|
+
pcm16: "audio/pcm",
|
|
82
|
+
};
|
|
83
|
+
const documentTypes = {
|
|
84
|
+
pdf: "application/pdf",
|
|
85
|
+
csv: "text/csv",
|
|
86
|
+
doc: "application/msword",
|
|
87
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
88
|
+
xls: "application/vnd.ms-excel",
|
|
89
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
90
|
+
html: "text/html",
|
|
91
|
+
txt: "text/plain",
|
|
92
|
+
md: "text/markdown",
|
|
93
|
+
};
|
|
94
|
+
function object(value) {
|
|
95
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
96
|
+
? value
|
|
97
|
+
: undefined;
|
|
74
98
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return part.type === "blob" ? "content" : part.type === "file" ? "data" : undefined;
|
|
99
|
+
function string(value) {
|
|
100
|
+
return typeof value === "string" ? value : undefined;
|
|
78
101
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const inline = key === undefined ? undefined : part[key];
|
|
89
|
-
if (key !== undefined && typeof inline === "string") {
|
|
90
|
-
const bytes = fileBytes(inline);
|
|
91
|
-
if (bytes.byteLength > INLINE_FILE_LIMIT) {
|
|
92
|
-
const { [key]: _omitted, ...rest } = part;
|
|
93
|
-
state.changed = true;
|
|
94
|
-
state.files++;
|
|
95
|
-
return {
|
|
96
|
-
...rest,
|
|
97
|
-
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
98
|
-
size: bytes.byteLength,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return Object.fromEntries(Object.entries(part).map(([name, item]) => [name, hashNode(item, state, depth + 1)]));
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Replaces inline files longer than {@link INLINE_FILE_LIMIT} in a recorded message attribute
|
|
106
|
-
* with their SHA-256 and byte size, so a span that inlines a large file exports the file's
|
|
107
|
-
* identity instead of being rejected for its size. The part keeps its other fields (`type`,
|
|
108
|
-
* `mime_type`, `modality`, `mediaType`, …). Other attributes, shorter messages and values that
|
|
109
|
-
* are not JSON are returned unchanged.
|
|
110
|
-
*/
|
|
111
|
-
export function hashInlineFiles(key, value,
|
|
112
|
-
/** Counts the files replaced by their digest, when given. */
|
|
113
|
-
count) {
|
|
114
|
-
if (!messageKeys.has(key) ||
|
|
115
|
-
typeof value !== "string" ||
|
|
116
|
-
Buffer.byteLength(value, "utf8") <= INLINE_FILE_LIMIT ||
|
|
117
|
-
Buffer.byteLength(value, "utf8") > MAX_INLINE_FILE_TEXT ||
|
|
118
|
-
!(value.includes('"blob"') || value.includes('"file"')))
|
|
119
|
-
return value;
|
|
120
|
-
try {
|
|
121
|
-
const parsed = JSON.parse(value);
|
|
122
|
-
const state = { changed: false, files: 0 };
|
|
123
|
-
const hashed = hashNode(parsed, state, 0);
|
|
124
|
-
if (count)
|
|
125
|
-
count.files += state.files;
|
|
126
|
-
return state.changed ? JSON.stringify(hashed) : value;
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
// Not JSON, or nested too deeply to inspect: export decides the value's fate as before.
|
|
130
|
-
return value;
|
|
131
|
-
}
|
|
102
|
+
export function isMediaName(value) {
|
|
103
|
+
return (typeof value === "string" &&
|
|
104
|
+
value.trim().length > 0 &&
|
|
105
|
+
value.length <= 256 &&
|
|
106
|
+
value.isWellFormed() &&
|
|
107
|
+
!Array.from(value).some((character) => {
|
|
108
|
+
const code = character.charCodeAt(0);
|
|
109
|
+
return code < 32 || code === 127;
|
|
110
|
+
}));
|
|
132
111
|
}
|
|
133
112
|
function pointerSegment(segment) {
|
|
134
113
|
return segment.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
135
114
|
}
|
|
136
|
-
/**
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
115
|
+
/** Whether the receiver can preview these bytes as text. */
|
|
116
|
+
export function isTextMediaType(contentType) {
|
|
117
|
+
const type = contentType.split(";", 1)[0].trim().toLowerCase();
|
|
118
|
+
return (type.startsWith("text/") ||
|
|
119
|
+
type === "application/json" ||
|
|
120
|
+
type.endsWith("+json") ||
|
|
121
|
+
type === "application/xml" ||
|
|
122
|
+
type.endsWith("+xml") ||
|
|
123
|
+
type === "application/yaml" ||
|
|
124
|
+
type === "application/x-yaml" ||
|
|
125
|
+
type === "application/javascript" ||
|
|
126
|
+
type === "application/x-javascript" ||
|
|
127
|
+
type === "application/x-www-form-urlencoded");
|
|
128
|
+
}
|
|
129
|
+
/** Cheap shared prefilter before bounded parsing of span message attributes. */
|
|
130
|
+
export function mayInlineFiles(key, value) {
|
|
131
|
+
return (isMessageKey(key) &&
|
|
132
|
+
typeof value === "string" &&
|
|
133
|
+
[
|
|
134
|
+
"blob",
|
|
135
|
+
"file",
|
|
136
|
+
"image",
|
|
137
|
+
"document",
|
|
138
|
+
"media",
|
|
139
|
+
"inlineData",
|
|
140
|
+
"inline_data",
|
|
141
|
+
"input_audio",
|
|
142
|
+
"\\u",
|
|
143
|
+
].some((marker) => value.includes(marker)));
|
|
144
|
+
}
|
|
145
|
+
/** A printable MIME type with optional parameters, accepted by the upload contract. */
|
|
146
|
+
export function isMediaContentType(value) {
|
|
147
|
+
return (typeof value === "string" &&
|
|
148
|
+
value.length >= 3 &&
|
|
149
|
+
value.length <= 255 &&
|
|
150
|
+
/^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9!#$&^_.+-]+(?: *;[ -~]*)?$/.test(value));
|
|
151
|
+
}
|
|
152
|
+
/** Detect the content pointer, MIME, name, and encoding contract of a provider media part. */
|
|
153
|
+
export function contentPath(part, pointer = "") {
|
|
154
|
+
let owner = part;
|
|
155
|
+
let key;
|
|
156
|
+
let path = "";
|
|
157
|
+
let contentType = string(part.mime_type ?? part.mimeType ?? part.mediaType ?? part.media_type);
|
|
158
|
+
let name = string(part.name ?? part.filename);
|
|
159
|
+
let encoded = false;
|
|
160
|
+
let dataUrlOnly = false;
|
|
161
|
+
const source = object(part.source);
|
|
162
|
+
const inline = object(part.inlineData ?? part.inline_data);
|
|
163
|
+
const audio = object(part.input_audio);
|
|
164
|
+
const chatFile = object(part.file);
|
|
165
|
+
const bedrock = object(part.image ?? part.document);
|
|
166
|
+
if (inline) {
|
|
167
|
+
owner = inline;
|
|
168
|
+
key = "data";
|
|
169
|
+
path = part.inlineData ? "/inlineData" : "/inline_data";
|
|
170
|
+
contentType = string(inline.mimeType ?? inline.mime_type);
|
|
171
|
+
encoded = true;
|
|
172
|
+
name = undefined;
|
|
173
|
+
}
|
|
174
|
+
else if (audio && part.type === "input_audio") {
|
|
175
|
+
owner = audio;
|
|
176
|
+
key = "data";
|
|
177
|
+
path = "/input_audio";
|
|
178
|
+
contentType = audioTypes[string(audio.format) ?? ""];
|
|
179
|
+
encoded = true;
|
|
180
|
+
name = undefined;
|
|
181
|
+
}
|
|
182
|
+
else if (chatFile && part.type === "file") {
|
|
183
|
+
owner = chatFile;
|
|
184
|
+
key = "file_data";
|
|
185
|
+
path = "/file";
|
|
186
|
+
name = string(chatFile.filename);
|
|
187
|
+
dataUrlOnly = true;
|
|
188
|
+
}
|
|
189
|
+
else if (bedrock && object(bedrock.source)) {
|
|
190
|
+
const isImage = object(part.image) !== undefined;
|
|
191
|
+
owner = object(bedrock.source);
|
|
192
|
+
key = "bytes";
|
|
193
|
+
path = isImage ? "/image/source" : "/document/source";
|
|
194
|
+
const format = string(bedrock.format) ?? "";
|
|
195
|
+
contentType =
|
|
196
|
+
isImage && ["png", "jpeg", "gif", "webp"].includes(format)
|
|
197
|
+
? `image/${format}`
|
|
198
|
+
: documentTypes[format];
|
|
199
|
+
name = isImage ? undefined : string(bedrock.name);
|
|
200
|
+
encoded = true;
|
|
201
|
+
}
|
|
202
|
+
else if (source && ["image", "document"].includes(string(part.type) ?? "")) {
|
|
203
|
+
if (source.type !== "base64")
|
|
204
|
+
return undefined;
|
|
205
|
+
owner = source;
|
|
206
|
+
key = "data";
|
|
207
|
+
path = "/source";
|
|
208
|
+
contentType = string(source.media_type);
|
|
209
|
+
name = string(part.title);
|
|
210
|
+
encoded = true;
|
|
211
|
+
}
|
|
212
|
+
else if (part.type === "image_url") {
|
|
213
|
+
const image = object(part.image_url);
|
|
214
|
+
owner = image ?? part;
|
|
215
|
+
key = image ? "url" : "image_url";
|
|
216
|
+
path = image ? "/image_url" : "";
|
|
217
|
+
contentType = undefined;
|
|
218
|
+
name = undefined;
|
|
219
|
+
dataUrlOnly = true;
|
|
220
|
+
}
|
|
221
|
+
else if (part.type === "input_image") {
|
|
222
|
+
key = "image_url";
|
|
223
|
+
contentType = undefined;
|
|
224
|
+
name = undefined;
|
|
225
|
+
dataUrlOnly = true;
|
|
226
|
+
}
|
|
227
|
+
else if (part.type === "input_file") {
|
|
228
|
+
key = "file_data";
|
|
229
|
+
contentType = undefined;
|
|
230
|
+
name = string(part.filename);
|
|
231
|
+
dataUrlOnly = true;
|
|
232
|
+
}
|
|
233
|
+
else if (part.type === "image") {
|
|
234
|
+
key = "image";
|
|
235
|
+
name = undefined;
|
|
236
|
+
}
|
|
237
|
+
else if (part.type === "media") {
|
|
238
|
+
key = "data";
|
|
239
|
+
name = undefined;
|
|
240
|
+
}
|
|
241
|
+
else
|
|
242
|
+
key = part.type === "blob" ? "content" : part.type === "file" ? "data" : undefined;
|
|
243
|
+
if (!key)
|
|
244
|
+
return undefined;
|
|
245
|
+
const content = owner[key];
|
|
246
|
+
if (typeof content !== "string" || /^(?:https?:|s3:|gs:|file:|blob:)/i.test(content))
|
|
247
|
+
return undefined;
|
|
248
|
+
if (dataUrlOnly && !content.startsWith("data:"))
|
|
249
|
+
return undefined;
|
|
250
|
+
// A declared base64 source must actually decode; malformed payloads stay as recorded.
|
|
251
|
+
if (encoded && !(content.length % 4 === 0 && base64.test(content)))
|
|
252
|
+
return undefined;
|
|
253
|
+
return {
|
|
254
|
+
part,
|
|
255
|
+
owner,
|
|
256
|
+
key,
|
|
257
|
+
content,
|
|
258
|
+
pointer: `${pointer}${path}/${key}`,
|
|
259
|
+
partPointer: pointer,
|
|
260
|
+
...(contentType ? { contentType } : {}),
|
|
261
|
+
...(isMediaName(name) ? { name } : {}),
|
|
262
|
+
...(encoded ? { base64: true } : {}),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** Inline media in document order. The caller applies the decoded text threshold. */
|
|
142
266
|
export function largeInlineFileParts(value) {
|
|
143
267
|
const found = [];
|
|
144
268
|
const visit = (node, pointer, depth) => {
|
|
@@ -148,17 +272,12 @@ export function largeInlineFileParts(value) {
|
|
|
148
272
|
node.forEach((item, index) => visit(item, `${pointer}/${index}`, depth + 1));
|
|
149
273
|
return;
|
|
150
274
|
}
|
|
151
|
-
|
|
275
|
+
const part = object(node);
|
|
276
|
+
if (!part)
|
|
152
277
|
return;
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// Decoding only shrinks the text, so text of at most the limit holds no file over it.
|
|
157
|
-
if (key !== undefined &&
|
|
158
|
-
typeof inline === "string" &&
|
|
159
|
-
inline.length * 3 > INLINE_FILE_LIMIT &&
|
|
160
|
-
Buffer.byteLength(inline, "utf8") > INLINE_FILE_LIMIT) {
|
|
161
|
-
found.push({ part, key, content: inline, pointer: `${pointer}/${key}` });
|
|
278
|
+
const file = contentPath(part, pointer);
|
|
279
|
+
if (file) {
|
|
280
|
+
found.push(file);
|
|
162
281
|
return;
|
|
163
282
|
}
|
|
164
283
|
for (const [name, item] of Object.entries(part))
|
|
@@ -167,10 +286,96 @@ export function largeInlineFileParts(value) {
|
|
|
167
286
|
visit(value, "", 0);
|
|
168
287
|
return found;
|
|
169
288
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
289
|
+
export function mediaBytes(file) {
|
|
290
|
+
if (file.base64)
|
|
291
|
+
return { bytes: Buffer.from(file.content, "base64"), decoded: true };
|
|
292
|
+
if (isMediaContentType(file.contentType) &&
|
|
293
|
+
isTextMediaType(file.contentType) &&
|
|
294
|
+
!file.content.startsWith("data:"))
|
|
295
|
+
return { bytes: Buffer.from(file.content, "utf8"), decoded: false };
|
|
296
|
+
return inlineFileBytes(file.content);
|
|
297
|
+
}
|
|
298
|
+
export function mediaContentType(file, decoded) {
|
|
299
|
+
return ((isMediaContentType(file.contentType) ? file.contentType : undefined) ??
|
|
300
|
+
(isMediaContentType(decoded.urlMediaType) ? decoded.urlMediaType : undefined) ??
|
|
301
|
+
(decoded.decoded ? "application/octet-stream" : "text/plain; charset=utf-8"));
|
|
302
|
+
}
|
|
303
|
+
/** Bounded payload identity memo, shared by admission and export, cleared at shutdown. */
|
|
304
|
+
export class MediaDigestMemo {
|
|
305
|
+
entries = new Map();
|
|
306
|
+
units = 0;
|
|
307
|
+
clear() {
|
|
308
|
+
this.entries.clear();
|
|
309
|
+
this.units = 0;
|
|
310
|
+
}
|
|
311
|
+
digest(file, decoded, traceId = "") {
|
|
312
|
+
return this.bytes((decoded ?? mediaBytes(file)).bytes, traceId);
|
|
313
|
+
}
|
|
314
|
+
bytes(bytes, traceId) {
|
|
315
|
+
const digest = {
|
|
316
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
317
|
+
size: bytes.byteLength,
|
|
318
|
+
};
|
|
319
|
+
const key = `${traceId}:${digest.sha256}`;
|
|
320
|
+
const known = this.entries.get(key);
|
|
321
|
+
if (known)
|
|
322
|
+
return known;
|
|
323
|
+
const limit = 32 * 1024 * 1024;
|
|
324
|
+
if (key.length <= limit) {
|
|
325
|
+
while (this.entries.size >= 4096 || this.units + key.length > limit) {
|
|
326
|
+
const first = this.entries.keys().next().value;
|
|
327
|
+
this.units -= first.length;
|
|
328
|
+
this.entries.delete(first);
|
|
329
|
+
}
|
|
330
|
+
this.entries.set(key, digest);
|
|
331
|
+
this.units += key.length;
|
|
332
|
+
}
|
|
333
|
+
return digest;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** Digest placement removes only the leaf and adds identity to the provider part. */
|
|
337
|
+
export function digestPart(file, digest) {
|
|
338
|
+
delete file.owner[file.key];
|
|
339
|
+
file.part.sha256 = digest.sha256;
|
|
340
|
+
file.part.size = digest.size;
|
|
341
|
+
}
|
|
342
|
+
export function hashInlineFiles(key, value, count, memo, traceId = "") {
|
|
343
|
+
return hashFiles(key, value, false, count, memo, traceId);
|
|
344
|
+
}
|
|
345
|
+
/** Nested message attributes retain small media and digest larger media without upload state. */
|
|
346
|
+
export function hashNestedInlineFiles(key, value) {
|
|
347
|
+
return hashFiles(key, value, true);
|
|
348
|
+
}
|
|
349
|
+
function hashFiles(key, value, nested, count, memo, traceId = "") {
|
|
350
|
+
if (typeof value !== "string" ||
|
|
351
|
+
!mayInlineFiles(key, value) ||
|
|
352
|
+
value.length > MAX_INLINE_FILE_TEXT)
|
|
353
|
+
return value;
|
|
354
|
+
try {
|
|
355
|
+
const parsed = JSON.parse(value);
|
|
356
|
+
const files = largeInlineFileParts(parsed);
|
|
357
|
+
let changed = false;
|
|
358
|
+
for (const file of files) {
|
|
359
|
+
const decoded = mediaBytes(file);
|
|
360
|
+
if (!decoded.bytes.byteLength)
|
|
361
|
+
continue;
|
|
362
|
+
if ((nested || isTextMediaType(mediaContentType(file, decoded))) &&
|
|
363
|
+
decoded.bytes.byteLength <= INLINE_FILE_LIMIT)
|
|
364
|
+
continue;
|
|
365
|
+
const digest = memo
|
|
366
|
+
? memo.digest(file, decoded, traceId)
|
|
367
|
+
: {
|
|
368
|
+
sha256: createHash("sha256").update(decoded.bytes).digest("hex"),
|
|
369
|
+
size: decoded.bytes.byteLength,
|
|
370
|
+
};
|
|
371
|
+
digestPart(file, digest);
|
|
372
|
+
changed = true;
|
|
373
|
+
if (count)
|
|
374
|
+
count.files++;
|
|
375
|
+
}
|
|
376
|
+
return changed ? JSON.stringify(parsed) : value;
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
176
381
|
}
|
package/dist/privacy.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface RedactionLimits {
|
|
|
33
33
|
valueBytes?: number;
|
|
34
34
|
/** The record's value strings cut when it was queued. */
|
|
35
35
|
cut?: ReadonlySet<string>;
|
|
36
|
-
/** The span's own messages whose
|
|
36
|
+
/** The span's own messages whose inline media were shrunk to their digest when it was
|
|
37
37
|
* queued, with how many files each: reported as not uploaded when values may be. */
|
|
38
38
|
digested?: ReadonlyMap<string, DigestedMessage> | undefined;
|
|
39
39
|
/**
|
package/dist/privacy.js
CHANGED
|
@@ -2,7 +2,7 @@ import { SpanStatusCode } from "@opentelemetry/api";
|
|
|
2
2
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
3
3
|
import { MAX_CONTENT_BYTES, MAX_RECORD_NODES, REDACTION_CONTEXT_UNITS } from "./config.js";
|
|
4
4
|
import { scrubToolCredentials, withToolCatalogSummary } from "./tool-definitions.js";
|
|
5
|
-
import { hashInlineFiles,
|
|
5
|
+
import { hashInlineFiles, mayInlineFiles } from "./inline-files.js";
|
|
6
6
|
import { truncateUtf8 } from "./safety.js";
|
|
7
7
|
export { truncateUtf8 };
|
|
8
8
|
/** Attribute keys (and their dotted children) removed in metadata-only mode. */
|
|
@@ -142,18 +142,12 @@ function redactedText(value, path, options, cap) {
|
|
|
142
142
|
throw new Error("Redaction produced unsupported text");
|
|
143
143
|
return result;
|
|
144
144
|
}
|
|
145
|
-
/** Whether a recorded message's text may inline a file over the inline file limit. */
|
|
146
|
-
function mayInlineFiles(key, text) {
|
|
147
|
-
return (isMessageKey(key) &&
|
|
148
|
-
text.length * 3 > INLINE_FILE_LIMIT &&
|
|
149
|
-
(text.includes('"blob"') || text.includes('"file"')));
|
|
150
|
-
}
|
|
151
145
|
/** Marks a value the upload step does not place. */
|
|
152
146
|
const NOT_HELD = Symbol("not held");
|
|
153
147
|
/**
|
|
154
148
|
* One of a span's own attribute values, when its values may be uploaded: text through the
|
|
155
149
|
* credential scrub and the redactor, whole, and collected when it is over the cap or may inline
|
|
156
|
-
*
|
|
150
|
+
* media; bytes over the cap collected as they are. {@link NOT_HELD} for any other value,
|
|
157
151
|
* and for text the queue cut when the record was admitted, which is never the whole value.
|
|
158
152
|
*/
|
|
159
153
|
function offloadValue(key, value, path, options, budget) {
|
|
@@ -168,7 +162,7 @@ function offloadValue(key, value, path, options, budget) {
|
|
|
168
162
|
}
|
|
169
163
|
if (typeof value !== "string")
|
|
170
164
|
return NOT_HELD;
|
|
171
|
-
// Each
|
|
165
|
+
// Each inline media part the queue shrank to its digest is reported as not uploaded; what is
|
|
172
166
|
// left of the message is placed like any other value, and reported too if it is cut.
|
|
173
167
|
const digested = budget.digested?.get(key);
|
|
174
168
|
const files = digested?.text === value ? digested.files : 0;
|
|
@@ -271,7 +265,7 @@ function attributes(source, options, path, budget, listPrefix = "", own = false)
|
|
|
271
265
|
return [
|
|
272
266
|
[
|
|
273
267
|
key,
|
|
274
|
-
redactValue(scrubToolCredentials(key, hashInlineFiles(key, value)), `${path}.${key}`, options, budget, 0, `${listPrefix}${key}`),
|
|
268
|
+
redactValue(scrubToolCredentials(key, own ? hashInlineFiles(key, value) : value), `${path}.${key}`, options, budget, 0, `${listPrefix}${key}`),
|
|
275
269
|
],
|
|
276
270
|
];
|
|
277
271
|
}));
|
package/dist/safety.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Span } from "@opentelemetry/api";
|
|
2
2
|
export declare function noopSpan(): Span;
|
|
3
3
|
export declare function safeSpan(source: Span, failed: () => void): Span;
|
|
4
|
+
export declare function spanSource(span: Span): Span;
|
|
4
5
|
/** The bounds `encodeContent` enforces while it copies a value. */
|
|
5
6
|
export interface EncodeLimits {
|
|
6
7
|
bytes: number;
|
package/dist/safety.js
CHANGED
|
@@ -96,8 +96,14 @@ class SafeSpan {
|
|
|
96
96
|
this.write(() => this.source.recordException(...args));
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
const spanSources = new WeakMap();
|
|
99
100
|
export function safeSpan(source, failed) {
|
|
100
|
-
|
|
101
|
+
const span = new SafeSpan(source, failed);
|
|
102
|
+
spanSources.set(span, spanSource(source));
|
|
103
|
+
return span;
|
|
104
|
+
}
|
|
105
|
+
export function spanSource(span) {
|
|
106
|
+
return spanSources.get(span) ?? span;
|
|
101
107
|
}
|
|
102
108
|
/** One content attribute's bounds: the value cap, and as many values as the Python SDK's content
|
|
103
109
|
* snapshot allows, so a structured value of small members is cut at the cap rather than refused
|
package/dist/snapshot.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ReadableSpan } from "@opentelemetry/sdk-trace";
|
|
2
2
|
import type { ReadableLogRecord, ReadWriteLogRecord } from "@opentelemetry/sdk-logs";
|
|
3
|
+
import { type MediaDigestMemo } from "./inline-files.js";
|
|
3
4
|
/**
|
|
4
5
|
* How a span's own values over the receiver's value cap are held whole for export to upload:
|
|
5
6
|
* the cap, the bytes the held-value budget has left, and whether nothing else is held (a single
|
|
@@ -12,7 +13,7 @@ export interface HoldLimits {
|
|
|
12
13
|
budget: number;
|
|
13
14
|
first: boolean;
|
|
14
15
|
}
|
|
15
|
-
/** A span's own message whose
|
|
16
|
+
/** A span's own message whose inline media shrank to their digest when it was queued: the
|
|
16
17
|
* text queued, and how many files it lost. */
|
|
17
18
|
export interface DigestedMessage {
|
|
18
19
|
text: string;
|
|
@@ -20,7 +21,7 @@ export interface DigestedMessage {
|
|
|
20
21
|
}
|
|
21
22
|
/** What admission keeps of a record: its copy, the bytes charged for it, whether resource
|
|
22
23
|
* attributes were still unresolved, the value strings it cut, the bytes of the values it held
|
|
23
|
-
* whole for upload, and the span's own messages whose
|
|
24
|
+
* whole for upload, and the span's own messages whose inline media it shrank to their
|
|
24
25
|
* digest (with how many files each). */
|
|
25
26
|
export interface RecordSnapshot<T> {
|
|
26
27
|
record: T;
|
|
@@ -29,6 +30,8 @@ export interface RecordSnapshot<T> {
|
|
|
29
30
|
cut: ReadonlySet<string>;
|
|
30
31
|
held: number;
|
|
31
32
|
digested: ReadonlyMap<string, DigestedMessage>;
|
|
33
|
+
/** The span's own message attributes admission inspected or left to export to inspect. */
|
|
34
|
+
inspected: ReadonlySet<string>;
|
|
32
35
|
}
|
|
33
|
-
export declare function snapshotSpan(source: ReadableSpan, limit: number, captureContent?: boolean, valueUnits?: number, hold?: HoldLimits): RecordSnapshot<ReadableSpan>;
|
|
36
|
+
export declare function snapshotSpan(source: ReadableSpan, limit: number, captureContent?: boolean, valueUnits?: number, hold?: HoldLimits, memo?: MediaDigestMemo): RecordSnapshot<ReadableSpan>;
|
|
34
37
|
export declare function snapshotLog(source: ReadableLogRecord, limit: number, captureContent?: boolean, valueUnits?: number): RecordSnapshot<ReadWriteLogRecord>;
|