@hue-run/sdk 0.21.1 → 0.22.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.
Files changed (72) hide show
  1. package/CLI.md +246 -52
  2. package/README.md +54 -13
  3. package/WORLDS.md +6 -3
  4. package/dist/ai-sdk.d.ts +23 -1
  5. package/dist/ai-sdk.js +60 -2
  6. package/dist/cli/import-formats/adapter.d.ts +80 -0
  7. package/dist/cli/import-formats/adapter.js +1 -0
  8. package/dist/cli/import-formats/agent-events.d.ts +5 -0
  9. package/dist/cli/import-formats/agent-events.js +16 -0
  10. package/dist/cli/import-formats/atif.d.ts +3 -0
  11. package/dist/cli/import-formats/atif.js +15 -0
  12. package/dist/cli/import-formats/braintrust.d.ts +2 -0
  13. package/dist/cli/import-formats/braintrust.js +222 -0
  14. package/dist/cli/import-formats/containers.d.ts +32 -0
  15. package/dist/cli/import-formats/containers.js +399 -0
  16. package/dist/cli/import-formats/datadog.d.ts +2 -0
  17. package/dist/cli/import-formats/datadog.js +264 -0
  18. package/dist/cli/import-formats/helicone.d.ts +2 -0
  19. package/dist/cli/import-formats/helicone.js +161 -0
  20. package/dist/cli/import-formats/identity.d.ts +112 -0
  21. package/dist/cli/import-formats/identity.js +594 -0
  22. package/dist/cli/import-formats/jaeger.d.ts +3 -0
  23. package/dist/cli/import-formats/jaeger.js +266 -0
  24. package/dist/cli/import-formats/langfuse.d.ts +7 -0
  25. package/dist/cli/import-formats/langfuse.js +510 -0
  26. package/dist/cli/import-formats/langsmith.d.ts +2 -0
  27. package/dist/cli/import-formats/langsmith.js +244 -0
  28. package/dist/cli/import-formats/messages.d.ts +28 -0
  29. package/dist/cli/import-formats/messages.js +301 -0
  30. package/dist/cli/import-formats/mlflow.d.ts +2 -0
  31. package/dist/cli/import-formats/mlflow.js +260 -0
  32. package/dist/cli/import-formats/openai-agents.d.ts +2 -0
  33. package/dist/cli/import-formats/openai-agents.js +221 -0
  34. package/dist/cli/import-formats/opik.d.ts +2 -0
  35. package/dist/cli/import-formats/opik.js +193 -0
  36. package/dist/cli/import-formats/otel-family.d.ts +5 -0
  37. package/dist/cli/import-formats/otel-family.js +17 -0
  38. package/dist/cli/import-formats/otel-spans.d.ts +94 -0
  39. package/dist/cli/import-formats/otel-spans.js +262 -0
  40. package/dist/cli/import-formats/otlp-protobuf.d.ts +35 -0
  41. package/dist/cli/import-formats/otlp-protobuf.js +486 -0
  42. package/dist/cli/import-formats/otlp.d.ts +4 -0
  43. package/dist/cli/import-formats/otlp.js +15 -0
  44. package/dist/cli/import-formats/phoenix.d.ts +3 -0
  45. package/dist/cli/import-formats/phoenix.js +256 -0
  46. package/dist/cli/import-formats/raindrop.d.ts +21 -0
  47. package/dist/cli/import-formats/raindrop.js +255 -0
  48. package/dist/cli/import-formats/reader.d.ts +56 -0
  49. package/dist/cli/import-formats/reader.js +364 -0
  50. package/dist/cli/import-formats/registry.d.ts +36 -0
  51. package/dist/cli/import-formats/registry.js +71 -0
  52. package/dist/cli/import-formats/row-platform-fields.d.ts +32 -0
  53. package/dist/cli/import-formats/row-platform-fields.js +76 -0
  54. package/dist/cli/import-formats/rows.d.ts +165 -0
  55. package/dist/cli/import-formats/rows.js +638 -0
  56. package/dist/cli/import-formats/transcripts.d.ts +25 -0
  57. package/dist/cli/import-formats/transcripts.js +498 -0
  58. package/dist/cli/import-formats/weave.d.ts +2 -0
  59. package/dist/cli/import-formats/weave.js +159 -0
  60. package/dist/cli/import-formats/zipkin.d.ts +2 -0
  61. package/dist/cli/import-formats/zipkin.js +219 -0
  62. package/dist/cli/import.d.ts +1 -1
  63. package/dist/cli/import.js +4 -2
  64. package/dist/cli/traces-import-convert.d.ts +65 -33
  65. package/dist/cli/traces-import-convert.js +211 -214
  66. package/dist/cli/traces-import-prepare.d.ts +26 -22
  67. package/dist/cli/traces-import-prepare.js +51 -122
  68. package/dist/cli/traces-import.d.ts +4 -4
  69. package/dist/cli/traces-import.js +63 -11
  70. package/dist/version.d.ts +1 -1
  71. package/dist/version.js +1 -1
  72. package/package.json +1 -1
@@ -0,0 +1,510 @@
1
+ import { ConversionError, conversionFailure, isObject, langfuseEvaluationEnvironment, LangfuseTraceCollector, parseNestedJsonText, validateSource, } from "../traces-import-convert.js";
2
+ import { identityText } from "./identity.js";
3
+ import { readRecords, RowTraceCollector, rowTraceId } from "./rows.js";
4
+ const object = (value) => isObject(value) ? value : undefined;
5
+ const text = (value) => typeof value === "string" && value ? value : undefined;
6
+ /** A field under its camelCase (API, UI) or snake_case (blob storage) name. */
7
+ const field = (record, ...names) => {
8
+ for (const name of names)
9
+ if (record[name] !== undefined && record[name] !== null)
10
+ return record[name];
11
+ return undefined;
12
+ };
13
+ // --- Detection ----------------------------------------------------------------------------------
14
+ const isObservation = (value) => isObject(value) &&
15
+ typeof value.id === "string" &&
16
+ typeof value.traceId === "string" &&
17
+ typeof value.type === "string" &&
18
+ typeof value.startTime === "string";
19
+ /** Hue's documented observation list: every record an observation without Langfuse's project id. */
20
+ const documented = (value) => Array.isArray(value) &&
21
+ value.length > 0 &&
22
+ value.every((record) => isObservation(record) && !("projectId" in record));
23
+ /** An observation as Langfuse's API and UI write it (`traceId` may be null in the v2 API). */
24
+ const isCamelObservation = (value) => isObject(value) &&
25
+ typeof value.id === "string" &&
26
+ typeof value.type === "string" &&
27
+ typeof value.startTime === "string" &&
28
+ "traceId" in value;
29
+ /** An observation row of a blob storage export (`observations`, `observations_v2`). */
30
+ const isSnakeObservation = (value) => isObject(value) &&
31
+ typeof value.id === "string" &&
32
+ typeof value.type === "string" &&
33
+ typeof value.start_time === "string" &&
34
+ "trace_id" in value &&
35
+ "parent_observation_id" in value;
36
+ /** A trace: an API trace (with or without its observations), a traces-table download or blob
37
+ * row. Every one carries `bookmarked` and `public`. */
38
+ const isTraceRecord = (value) => isObject(value) &&
39
+ typeof value.id === "string" &&
40
+ typeof value.timestamp === "string" &&
41
+ "bookmarked" in value &&
42
+ "public" in value &&
43
+ !("traceId" in value) &&
44
+ !("trace_id" in value);
45
+ const isRecord = (value) => isCamelObservation(value) || isSnakeObservation(value) || isTraceRecord(value);
46
+ /** The lists of Langfuse records a page or envelope holds: `data` (list pages), `observations`
47
+ * (the native envelope) and `traces` (a session). One record suffices: the others are read one by
48
+ * one, and a malformed one refuses only its trace. */
49
+ const lists = (value) => isObject(value)
50
+ ? ["data", "observations", "traces"]
51
+ .map((key) => value[key])
52
+ .filter((list) => Array.isArray(list) && list.some(isRecord))
53
+ : [];
54
+ // --- langfuse_observations ----------------------------------------------------------------------
55
+ /** Hue's documented observation list: a list page (`data`) or the native `observations`
56
+ * envelope, every record an observation, converted as Hue's own reader converts it. */
57
+ export const langfuseObservations = {
58
+ id: "langfuse_observations",
59
+ manifestFormat: "langfuse_observations",
60
+ detect: (value) => isObject(value) &&
61
+ !isTraceRecord(value) &&
62
+ (documented(value.data) || documented(value.observations)),
63
+ convert(value, context) {
64
+ if (!isObject(value))
65
+ return;
66
+ if (value.data !== undefined && value.observations !== undefined)
67
+ throw new ConversionError("two observation envelopes in one file");
68
+ const key = documented(value.data) ? "data" : "observations";
69
+ let records = value[key];
70
+ const envelope = Object.fromEntries(Object.entries(value).filter(([name]) => name !== key));
71
+ const collector = context.collector(() => new LangfuseTraceCollector());
72
+ if (context.source.unsafeIntegers) {
73
+ // Which observations hold such an integer: read again with each marked, so only their
74
+ // traces are refused and the page's others read.
75
+ const marked = markedRecords(context.source.text, key);
76
+ if (!marked || marked.envelope)
77
+ throw new ConversionError("Source JSON contains an unsafe number");
78
+ const refused = new Set();
79
+ for (const [index, observation] of records.entries())
80
+ if (marked.records.has(index))
81
+ refused.add(observation.traceId);
82
+ for (const traceId of refused)
83
+ collector.refuse(traceId, "Source JSON contains an unsafe number", context.source.digest);
84
+ records = records.filter((observation) => !refused.has(observation.traceId));
85
+ if (!records.length)
86
+ return;
87
+ }
88
+ collector.add(records, envelope, context.source.digest);
89
+ },
90
+ };
91
+ /** An integer JavaScript cannot represent exactly, as `markedRecords` marks one. */
92
+ class UnsafeInteger {
93
+ }
94
+ /** The positions of the records of `text`'s list `key` that hold an integer JavaScript cannot
95
+ * represent exactly, and whether the rest of the document holds one; undefined when the text does
96
+ * not parse so. */
97
+ function markedRecords(text, key) {
98
+ let value;
99
+ try {
100
+ value = JSON.parse(text, (_key, item, source) => typeof item === "number" &&
101
+ !Number.isSafeInteger(item) &&
102
+ typeof source?.source === "string" &&
103
+ /^-?\d+$/.test(source.source)
104
+ ? new UnsafeInteger()
105
+ : item);
106
+ }
107
+ catch {
108
+ return undefined;
109
+ }
110
+ const holds = (item) => {
111
+ const stack = [item];
112
+ while (stack.length) {
113
+ const next = stack.pop();
114
+ if (next instanceof UnsafeInteger)
115
+ return true;
116
+ if (next !== null && typeof next === "object")
117
+ for (const child of Object.values(next))
118
+ stack.push(child);
119
+ }
120
+ return false;
121
+ };
122
+ if (!isObject(value) || !Array.isArray(value[key]))
123
+ return undefined;
124
+ const records = new Set();
125
+ for (const [index, record] of value[key].entries())
126
+ if (holds(record))
127
+ records.add(index);
128
+ const envelope = Object.entries(value).some(([name, item]) => name !== key && holds(item));
129
+ return { records, envelope };
130
+ }
131
+ // --- langfuse: reading a record -----------------------------------------------------------------
132
+ const FORMAT = "langfuse";
133
+ /** The longest source id the emitter keeps, less the root's `t-` prefix. */
134
+ const MAX_ID = 4094;
135
+ const KINDS = {
136
+ GENERATION: "llm",
137
+ TOOL: "tool",
138
+ AGENT: "agent",
139
+ EMBEDDING: "embedding",
140
+ RETRIEVER: "retrieval",
141
+ };
142
+ /** A judge assessing an output: Langfuse's evaluation inside a trace, and everything beneath it. */
143
+ const EVALUATOR = "EVALUATOR";
144
+ const EVALUATION_KEYS = ["hue.execution.id", "hue.eval.execution_id"];
145
+ const SIMULATION_KEYS = ["hue.world.id", "hue.environment.run_id"];
146
+ const INSTANT = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}(?::?\d{2})?)?$/i;
147
+ /** An ISO 8601 or ClickHouse instant as Unix nanoseconds; one without a zone is UTC. */
148
+ function instant(value, what) {
149
+ const match = typeof value === "string" ? INSTANT.exec(value.trim()) : null;
150
+ const zone = match?.[4]?.toUpperCase() ?? "Z";
151
+ const offset = zone === "Z"
152
+ ? zone
153
+ : zone.length === 3
154
+ ? `${zone}:00`
155
+ : `${zone.slice(0, 3)}:${zone.slice(-2)}`;
156
+ const millis = match ? Date.parse(`${match[1]}T${match[2]}${offset}`) : NaN;
157
+ if (!Number.isSafeInteger(millis) || millis <= 0)
158
+ throw new ConversionError(`${what} is not a timestamp`);
159
+ return BigInt(millis) * 1000000n + BigInt((match[3] ?? "").padEnd(9, "0"));
160
+ }
161
+ /** A copy's revision: its `updatedAt` in nanoseconds, when it is an instant. */
162
+ function revisionOf(record) {
163
+ try {
164
+ const updated = field(record, "updatedAt", "updated_at");
165
+ return updated === undefined ? undefined : instant(updated, "updatedAt").toString();
166
+ }
167
+ catch {
168
+ return undefined;
169
+ }
170
+ }
171
+ /** JSON text read as the JSON it holds (an object, list or string), else the value as recorded. */
172
+ function decoded(value) {
173
+ if (value === undefined || value === null || value === "")
174
+ return undefined;
175
+ if (typeof value !== "string" || !/^\s*[[{"]/.test(value))
176
+ return value;
177
+ try {
178
+ const parsed = parseNestedJsonText(value);
179
+ return parsed.unsafeIntegers ? value : parsed.value;
180
+ }
181
+ catch {
182
+ return value;
183
+ }
184
+ }
185
+ /** Metadata with each value recorded as JSON text read as its JSON, as Langfuse reads it. */
186
+ function metadataOf(value) {
187
+ const metadata = decoded(value);
188
+ const fields = object(metadata);
189
+ if (!fields)
190
+ return metadata;
191
+ if (!Object.keys(fields).length)
192
+ return undefined;
193
+ return Object.fromEntries(Object.entries(fields).map(([key, item]) => {
194
+ if (typeof item !== "string")
195
+ return [key, item];
196
+ try {
197
+ const parsed = parseNestedJsonText(item);
198
+ return [key, parsed.unsafeIntegers ? item : parsed.value];
199
+ }
200
+ catch {
201
+ return [key, item];
202
+ }
203
+ }));
204
+ }
205
+ /** Resource attributes Langfuse kept in metadata: flattened `resourceAttributes.<key>` (direct
206
+ * writes) or a `resourceAttributes` object (the legacy path). */
207
+ function resourceOf(metadata) {
208
+ const fields = object(metadata);
209
+ if (!fields)
210
+ return undefined;
211
+ const resource = { ...object(fields.resourceAttributes) };
212
+ for (const [key, value] of Object.entries(fields))
213
+ if (key.startsWith("resourceAttributes.") && key.length > 19)
214
+ resource[key.slice(19)] = value;
215
+ return Object.keys(resource).length ? resource : undefined;
216
+ }
217
+ /** Evaluation or simulation traffic: Hue's markers in metadata or resource attributes, or one of
218
+ * Langfuse's experiment and evaluator environments. */
219
+ function originOf(metadata, resource, environment) {
220
+ const fields = object(metadata) ?? {};
221
+ const keys = new Set([
222
+ ...Object.keys(fields).map((key) => key.replace(/^attributes\./, "")),
223
+ ...Object.keys(object(fields.attributes) ?? {}),
224
+ ...Object.keys(resource ?? {}),
225
+ ]);
226
+ if (EVALUATION_KEYS.some((key) => keys.has(key)) || langfuseEvaluationEnvironment(environment))
227
+ return "evaluation";
228
+ const scope = text(fields["scope.name"]) ?? text(object(fields.scope)?.name);
229
+ if (SIMULATION_KEYS.some((key) => keys.has(key)) || scope === "hue.sim-gateway")
230
+ return "simulation";
231
+ return undefined;
232
+ }
233
+ /** A token count: a nonnegative integer, or its decimal text (ClickHouse quotes 64-bit integers). */
234
+ function count(value) {
235
+ const number = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : value;
236
+ return typeof number === "number" && Number.isSafeInteger(number) && number >= 0
237
+ ? number
238
+ : undefined;
239
+ }
240
+ /** `usageDetails` as Langfuse totals it; `inputUsage`/`outputUsage` (a UI download's totals) when
241
+ * a record has no details. */
242
+ function usageOf(record) {
243
+ const counts = new Map();
244
+ for (const [key, value] of Object.entries(object(field(record, "usageDetails", "usage_details")) ?? {})) {
245
+ const tokens = count(value);
246
+ if (tokens !== undefined)
247
+ counts.set(key, tokens);
248
+ }
249
+ const sum = (match) => {
250
+ let total;
251
+ for (const [key, tokens] of counts)
252
+ if (match(key))
253
+ total = (total ?? 0) + tokens;
254
+ return total;
255
+ };
256
+ const first = (...keys) => keys.map((key) => counts.get(key)).find((tokens) => tokens !== undefined);
257
+ const usage = {
258
+ input: sum((key) => key.includes("input")) ??
259
+ first("prompt_tokens") ??
260
+ count(field(record, "inputUsage")),
261
+ output: sum((key) => key.includes("output")) ??
262
+ first("completion_tokens") ??
263
+ count(field(record, "outputUsage")),
264
+ cacheRead: first("input_cached_tokens", "input_cache_read"),
265
+ cacheWrite: first("input_cache_write", "input_cache_creation"),
266
+ reasoning: first("output_reasoning_tokens", "output_reasoning"),
267
+ };
268
+ return Object.values(usage).some((tokens) => tokens !== undefined) ? usage : undefined;
269
+ }
270
+ /** A Python `@observe` tool's input, `{"args": [], "kwargs": {…}}`, is its keyword arguments. */
271
+ function toolArguments(input) {
272
+ const fields = object(input);
273
+ if (fields &&
274
+ Object.keys(fields).length === 2 &&
275
+ Array.isArray(fields.args) &&
276
+ !fields.args.length &&
277
+ object(fields.kwargs))
278
+ return fields.kwargs;
279
+ return input;
280
+ }
281
+ function sourceId(value, what) {
282
+ const id = text(value);
283
+ if (!id || id.length > MAX_ID)
284
+ throw new ConversionError(`${what} is missing or too long`);
285
+ return id;
286
+ }
287
+ const strings = (value) => Array.isArray(value) && value.length && value.every((item) => typeof item === "string")
288
+ ? value
289
+ : undefined;
290
+ /** The trace-level fields Langfuse copies onto observations, kept on the trace's root. */
291
+ function traceAttributes(record, nameField, tagField) {
292
+ const name = text(field(record, ...nameField));
293
+ const tags = strings(field(record, ...tagField));
294
+ const release = text(field(record, "release"));
295
+ return {
296
+ ...(name ? { "langfuse.trace.name": name } : {}),
297
+ ...(tags ? { "langfuse.trace.tags": tags } : {}),
298
+ ...(release ? { "langfuse.release": release } : {}),
299
+ };
300
+ }
301
+ function observationRow(record, inheritedTrace) {
302
+ const traceKey = text(field(record, "traceId", "trace_id")) ??
303
+ inheritedTrace ??
304
+ (() => {
305
+ throw new ConversionError("an observation without a traceId");
306
+ })();
307
+ sourceId(traceKey, "the trace id");
308
+ const spanKey = sourceId(record.id, "the observation id");
309
+ const type = text(record.type)?.toUpperCase() ?? "SPAN";
310
+ const kind = Object.hasOwn(KINDS, type) ? KINDS[type] : undefined;
311
+ const parent = text(field(record, "parentObservationId", "parent_observation_id"));
312
+ const start = instant(field(record, "startTime", "start_time"), "startTime");
313
+ const endValue = field(record, "endTime", "end_time");
314
+ const end = endValue === undefined || endValue === "" ? undefined : instant(endValue, "endTime");
315
+ const metadata = metadataOf(record.metadata);
316
+ const resource = resourceOf(metadata);
317
+ const environment = text(record.environment);
318
+ const origin = originOf(metadata, resource, environment);
319
+ const level = text(record.level);
320
+ const statusMessage = text(field(record, "statusMessage", "status_message"));
321
+ const model = text(field(record, "model", "providedModelName", "provided_model_name"));
322
+ const input = decoded(record.input);
323
+ const output = decoded(record.output);
324
+ const appRoot = field(record, "isRootObservation", "is_root_observation") === true;
325
+ const revision = revisionOf(record);
326
+ const row = {
327
+ traceKey,
328
+ spanKey,
329
+ ...(parent ? { parentKey: parent } : {}),
330
+ name: text(record.name) ?? type.toLowerCase(),
331
+ ...(kind ? { kind } : {}),
332
+ start,
333
+ ...(end !== undefined ? { end } : {}),
334
+ ...(model ? { model } : {}),
335
+ ...(input !== undefined ? { input } : {}),
336
+ ...(output !== undefined ? { output } : {}),
337
+ ...(level === "ERROR" ? { error: statusMessage ? { message: statusMessage } : {} } : {}),
338
+ ...(type === EVALUATOR ? { scoring: true } : {}),
339
+ ...(environment && environment !== "default" ? { environment } : {}),
340
+ ...(origin ? { origin } : {}),
341
+ ...(resource ? { resource } : {}),
342
+ attributes: {
343
+ "langfuse.observation.type": type.toLowerCase(),
344
+ ...(level && level !== "DEFAULT" ? { "langfuse.observation.level": level } : {}),
345
+ ...(statusMessage && level !== "ERROR"
346
+ ? { "langfuse.observation.status_message": statusMessage }
347
+ : {}),
348
+ ...(metadata !== undefined
349
+ ? { "langfuse.observation.metadata": JSON.stringify(metadata) }
350
+ : {}),
351
+ ...(!parent || appRoot
352
+ ? traceAttributes(record, ["traceName", "trace_name"], ["tags", "traceTags"])
353
+ : {}),
354
+ },
355
+ record,
356
+ ...(metadata !== undefined ? { metadata } : {}),
357
+ ...(revision ? { revision } : {}),
358
+ };
359
+ if (kind === "tool") {
360
+ const args = toolArguments(input);
361
+ row.tool = {
362
+ name: row.name,
363
+ ...(args !== undefined ? { arguments: args } : {}),
364
+ ...(output !== undefined ? { result: output } : {}),
365
+ };
366
+ }
367
+ else {
368
+ const user = identityText(field(record, "userId", "user_id"));
369
+ const session = identityText(field(record, "sessionId", "session_id"));
370
+ if (user !== undefined)
371
+ row.user = user;
372
+ if (session !== undefined)
373
+ row.session = session;
374
+ const usage = usageOf(record);
375
+ if (usage)
376
+ row.usage = usage;
377
+ }
378
+ return { row, topLevel: !parent };
379
+ }
380
+ /** A trace record as the trace's root span, `t-<trace id>`. Embedded observations are their own
381
+ * spans' records and are left out of the root's. */
382
+ function traceRow(record) {
383
+ const traceKey = sourceId(record.id, "the trace id");
384
+ const metadata = metadataOf(record.metadata);
385
+ const resource = resourceOf(metadata);
386
+ const environment = text(record.environment);
387
+ const origin = originOf(metadata, resource, environment);
388
+ const name = text(record.name);
389
+ const version = text(record.version);
390
+ const user = identityText(field(record, "userId", "user_id"));
391
+ const session = identityText(field(record, "sessionId", "session_id"));
392
+ const input = decoded(record.input);
393
+ const output = decoded(record.output);
394
+ const embedded = Array.isArray(record.observations) && record.observations.some(isObject);
395
+ const kept = embedded
396
+ ? Object.fromEntries(Object.entries(record).filter(([key]) => key !== "observations"))
397
+ : record;
398
+ const revision = revisionOf(record);
399
+ return {
400
+ traceKey,
401
+ spanKey: `t-${traceKey}`,
402
+ name: name ?? "trace",
403
+ start: instant(record.timestamp, "timestamp"),
404
+ ...(input !== undefined ? { input } : {}),
405
+ ...(output !== undefined ? { output } : {}),
406
+ ...(user !== undefined ? { user } : {}),
407
+ ...(session !== undefined ? { session } : {}),
408
+ ...(environment && environment !== "default" ? { environment } : {}),
409
+ ...(origin ? { origin } : {}),
410
+ ...(resource ? { resource } : {}),
411
+ attributes: {
412
+ ...traceAttributes(record, ["name"], ["tags"]),
413
+ ...(version ? { "langfuse.version": version } : {}),
414
+ ...(metadata !== undefined ? { "langfuse.trace.metadata": JSON.stringify(metadata) } : {}),
415
+ },
416
+ record: kept,
417
+ ...(metadata !== undefined ? { metadata } : {}),
418
+ ...(revision ? { revision } : {}),
419
+ };
420
+ }
421
+ /** Gathers a run's Langfuse records by trace, so a trace record in one file roots the
422
+ * observations of another, and emits each trace through the shared emitter. */
423
+ class LangfuseCollector {
424
+ keys;
425
+ traces = new Map();
426
+ /** Its emitter writes `--user-key` and `--session-key` as every row format's does. */
427
+ appliesKeys = true;
428
+ constructor(keys) {
429
+ this.keys = keys;
430
+ }
431
+ add(pending) {
432
+ const held = this.traces.get(pending.row.traceKey);
433
+ if (held)
434
+ held.push(pending);
435
+ else
436
+ this.traces.set(pending.row.traceKey, [pending]);
437
+ }
438
+ finish() {
439
+ const emitter = new RowTraceCollector(FORMAT, this.keys);
440
+ const refused = [];
441
+ for (const [traceKey, pending] of this.traces) {
442
+ const root = `t-${traceKey}`;
443
+ // An observation named as the root is the root; a trace record then adds nothing.
444
+ const recordedRoot = pending.some((item) => !item.trace && item.row.spanKey === root);
445
+ const rooted = recordedRoot || pending.some((item) => item.trace);
446
+ try {
447
+ for (const item of pending) {
448
+ if (item.trace && recordedRoot)
449
+ continue;
450
+ const row = !item.trace && item.topLevel && rooted && item.row.spanKey !== root
451
+ ? { ...item.row, parentKey: root }
452
+ : item.row;
453
+ emitter.add(row, item.digest);
454
+ }
455
+ }
456
+ catch (error) {
457
+ const failure = conversionFailure(error);
458
+ if (!failure)
459
+ throw error;
460
+ refused.push({ traceId: rowTraceId(FORMAT, traceKey), reason: failure.message });
461
+ }
462
+ }
463
+ const finished = emitter.finish();
464
+ return { traces: finished.traces, refused: [...refused, ...finished.refused] };
465
+ }
466
+ }
467
+ /** A trace record and the observations embedded in it (`GET /api/public/traces/{id}`). */
468
+ function traceItems(trace) {
469
+ const embedded = Array.isArray(trace.observations) ? trace.observations.filter(isObject) : [];
470
+ return [{ record: trace }, ...embedded.map((record) => ({ record, trace: trace.id }))];
471
+ }
472
+ function itemsOf(value) {
473
+ if (isTraceRecord(value))
474
+ return traceItems(value);
475
+ if (isCamelObservation(value) || isSnakeObservation(value))
476
+ return [{ record: value }];
477
+ const [list, ...more] = lists(value);
478
+ if (more.length)
479
+ throw new ConversionError("two lists of Langfuse records in one value");
480
+ return (list ?? []).flatMap((record) => isTraceRecord(record) ? traceItems(record) : [{ record }]);
481
+ }
482
+ const traceKeyOf = ({ record, trace }) => {
483
+ if (!isObject(record))
484
+ return trace;
485
+ const fields = record;
486
+ return (text(field(fields, "traceId", "trace_id")) ??
487
+ (isTraceRecord(record) ? record.id : undefined) ??
488
+ trace);
489
+ };
490
+ /** Langfuse's own exports, from its API, UI downloads and blob storage (see the top of this
491
+ * file), as canonical rows. */
492
+ export const langfuse = {
493
+ id: FORMAT,
494
+ manifestFormat: "otlp_json",
495
+ detect: (value) => !langfuseObservations.detect(value) && (isRecord(value) || lists(value).length > 0),
496
+ convert(value, context) {
497
+ const collector = context.collector(() => new LangfuseCollector(context.keys));
498
+ readRecords(context, itemsOf(value), traceKeyOf, (item) => {
499
+ const { record } = item;
500
+ if (!isRecord(record))
501
+ throw new ConversionError("a record that is neither a Langfuse trace nor an observation");
502
+ validateSource(record);
503
+ const digest = context.source.digest;
504
+ if (isTraceRecord(record))
505
+ collector.add({ row: traceRow(record), digest, topLevel: false, trace: true });
506
+ else
507
+ collector.add({ ...observationRow(record, item.trace), digest, trace: false });
508
+ });
509
+ },
510
+ };
@@ -0,0 +1,2 @@
1
+ import type { ImportAdapter } from "./adapter.js";
2
+ export declare const langsmithRuns: ImportAdapter;