@hue-run/sdk 0.21.0 → 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 (74) hide show
  1. package/CLI.md +255 -61
  2. package/README.md +61 -20
  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/mcp.d.ts +5 -1
  65. package/dist/cli/mcp.js +35 -23
  66. package/dist/cli/traces-import-convert.d.ts +65 -33
  67. package/dist/cli/traces-import-convert.js +211 -214
  68. package/dist/cli/traces-import-prepare.d.ts +26 -22
  69. package/dist/cli/traces-import-prepare.js +51 -122
  70. package/dist/cli/traces-import.d.ts +4 -4
  71. package/dist/cli/traces-import.js +63 -11
  72. package/dist/version.d.ts +1 -1
  73. package/dist/version.js +1 -1
  74. package/package.json +3 -3
@@ -0,0 +1,256 @@
1
+ import { anyValue, ConversionError, isObject, unixNano, } from "../traces-import-convert.js";
2
+ import { otelTraceKey, OtelSpanCollector, unsigned } from "./otel-spans.js";
3
+ import { readRecords } from "./rows.js";
4
+ const object = (value) => isObject(value) ? value : undefined;
5
+ const text = (value) => typeof value === "string" && value ? value : undefined;
6
+ /** A page's records, or the value itself when it is one record. */
7
+ function recordsOf(value, isRecord) {
8
+ if (isRecord(value))
9
+ return [value];
10
+ const page = object(value);
11
+ return page && Array.isArray(page.data) && page.data.some(isRecord) ? page.data : undefined;
12
+ }
13
+ /** An OTLP id as hex: hex as recorded, or the base64 the OTLP JSON mapping allows; any other
14
+ * text is kept and mapped by SHA-256. */
15
+ function hexId(value, bytes) {
16
+ if (new RegExp(`^[0-9a-f]{${bytes * 2}}$`, "i").test(value))
17
+ return value.toLowerCase();
18
+ if (/^[A-Za-z0-9+/]+={0,2}$/.test(value)) {
19
+ const decoded = Buffer.from(value, "base64");
20
+ if (decoded.byteLength === bytes && decoded.toString("base64") === value)
21
+ return decoded.toString("hex");
22
+ }
23
+ return value;
24
+ }
25
+ /** A nanosecond instant: absent when null, refused when malformed or zero. */
26
+ function nanos(value, what) {
27
+ if (value === undefined || value === null)
28
+ return undefined;
29
+ const count = unsigned(value);
30
+ if (!count)
31
+ throw new ConversionError(`${what} is not a nanosecond instant`);
32
+ return count;
33
+ }
34
+ const KINDS = {
35
+ SPAN_KIND_UNSPECIFIED: 0,
36
+ SPAN_KIND_INTERNAL: 1,
37
+ SPAN_KIND_SERVER: 2,
38
+ SPAN_KIND_CLIENT: 3,
39
+ SPAN_KIND_PRODUCER: 4,
40
+ SPAN_KIND_CONSUMER: 5,
41
+ };
42
+ const STATUS = { UNSET: 0, OK: 1, ERROR: 2 };
43
+ /** An `OtlpAnyValue` in proto field names as OTLP/JSON. A dictionary, which Phoenix writes as an
44
+ * empty value, stays empty. */
45
+ function anyValueOf(raw) {
46
+ const value = object(raw);
47
+ if (raw === undefined || raw === null)
48
+ return {};
49
+ if (!value)
50
+ throw new ConversionError("an attribute value that is not an object");
51
+ if (typeof value.string_value === "string")
52
+ return { stringValue: value.string_value };
53
+ if (typeof value.bool_value === "boolean")
54
+ return { boolValue: value.bool_value };
55
+ if (value.int_value !== undefined && value.int_value !== null) {
56
+ const integer = value.int_value;
57
+ if (typeof integer === "number" && Number.isSafeInteger(integer))
58
+ return { intValue: String(integer) };
59
+ if (typeof integer === "string" && /^-?\d+$/.test(integer))
60
+ return { intValue: integer };
61
+ throw new ConversionError("an int_value that is not an integer");
62
+ }
63
+ if (typeof value.double_value === "number")
64
+ return { doubleValue: value.double_value };
65
+ if (typeof value.double_value === "string") {
66
+ // An infinite or undefined double, which Phoenix writes as Python prints it, is kept as its
67
+ // text: OTLP/JSON has no number for it.
68
+ if (!/^-?(?:inf|infinity|nan)$/i.test(value.double_value))
69
+ throw new ConversionError("a double_value that is not a number");
70
+ return { stringValue: value.double_value };
71
+ }
72
+ if (typeof value.bytes_value === "string")
73
+ return { bytesValue: value.bytes_value };
74
+ const array = object(value.array_value);
75
+ if (array) {
76
+ const values = array.values ?? [];
77
+ if (!Array.isArray(values))
78
+ throw new ConversionError("an array_value without values");
79
+ return { arrayValue: { values: values.map(anyValueOf) } };
80
+ }
81
+ return {};
82
+ }
83
+ function keyValues(raw) {
84
+ if (raw === undefined || raw === null)
85
+ return [];
86
+ if (!Array.isArray(raw))
87
+ throw new ConversionError("attributes is not a list");
88
+ return raw.map((item) => {
89
+ const pair = object(item);
90
+ if (!pair || typeof pair.key !== "string")
91
+ throw new ConversionError("an attribute without a key");
92
+ return { key: pair.key, value: anyValueOf(pair.value) };
93
+ });
94
+ }
95
+ const isOtlpSpan = (value) => isObject(value) &&
96
+ typeof value.trace_id === "string" &&
97
+ typeof value.span_id === "string" &&
98
+ ("start_time_unix_nano" in value || "end_time_unix_nano" in value) &&
99
+ !("context" in value) &&
100
+ !("root_span_id" in value);
101
+ function readOtlpSpan(span) {
102
+ const { trace_id: rawTraceId, span_id: rawSpanId } = span;
103
+ if (!isOtlpSpan(span) || typeof rawTraceId !== "string" || typeof rawSpanId !== "string")
104
+ throw new ConversionError("a record that is not a Phoenix OTLP span");
105
+ const traceId = hexId(rawTraceId, 16);
106
+ const parent = text(span.parent_span_id);
107
+ const kind = span.kind;
108
+ const status = object(span.status);
109
+ const code = status?.code;
110
+ const message = text(status?.message);
111
+ const start = nanos(span.start_time_unix_nano, "start_time_unix_nano");
112
+ const end = nanos(span.end_time_unix_nano, "end_time_unix_nano");
113
+ const events = span.events ?? [];
114
+ if (!Array.isArray(events))
115
+ throw new ConversionError("events is not a list");
116
+ const flags = unsigned(span.flags);
117
+ const dropped = unsigned(span.dropped_attributes_count);
118
+ return {
119
+ traceId,
120
+ spanKey: hexId(rawSpanId, 8),
121
+ ...(parent ? { parentKey: hexId(parent, 8) } : {}),
122
+ name: text(span.name) ?? "",
123
+ kind: typeof kind === "number" && Number.isInteger(kind) && kind >= 0 && kind <= 5
124
+ ? kind
125
+ : typeof kind === "string"
126
+ ? (KINDS[kind] ?? 1)
127
+ : 1,
128
+ ...(text(span.trace_state) ? { traceState: text(span.trace_state) } : {}),
129
+ ...(flags !== undefined && flags <= 0xffffffffn ? { flags: Number(flags) } : {}),
130
+ ...(start !== undefined ? { start } : {}),
131
+ ...(end !== undefined ? { end } : {}),
132
+ attributes: keyValues(span.attributes),
133
+ ...(dropped ? { droppedAttributesCount: Number(dropped) } : {}),
134
+ events: events.map((item) => {
135
+ const event = object(item);
136
+ if (!event)
137
+ throw new ConversionError("an event that is not an object");
138
+ const time = nanos(event.time_unix_nano, "an event time");
139
+ return {
140
+ name: text(event.name) ?? "",
141
+ ...(time ? { time } : {}),
142
+ attributes: keyValues(event.attributes),
143
+ };
144
+ }),
145
+ links: [],
146
+ ...(typeof code === "number" && code >= 0 && code <= 2
147
+ ? { status: { code, ...(message ? { message } : {}) } }
148
+ : {}),
149
+ resource: [],
150
+ record: span,
151
+ };
152
+ }
153
+ const isPhoenixSpan = (value) => {
154
+ const spanContext = object(object(value)?.context);
155
+ return (spanContext !== undefined &&
156
+ typeof spanContext.trace_id === "string" &&
157
+ typeof spanContext.span_id === "string" &&
158
+ isObject(value) &&
159
+ ("start_time" in value || "span_kind" in value));
160
+ };
161
+ /** A flattened attribute dictionary as OTLP attributes, the dotted keys kept. */
162
+ function dictionary(raw, what) {
163
+ if (raw === undefined || raw === null)
164
+ return [];
165
+ const values = object(raw);
166
+ if (!values)
167
+ throw new ConversionError(`${what} is not an object`);
168
+ return Object.entries(values).map(([key, value]) => ({ key, value: anyValue(value) }));
169
+ }
170
+ const instant = (value, what) => {
171
+ if (value === undefined || value === null)
172
+ return undefined;
173
+ try {
174
+ return BigInt(unixNano(value));
175
+ }
176
+ catch {
177
+ throw new ConversionError(`${what} is not a timestamp with a zone`);
178
+ }
179
+ };
180
+ function readPhoenixSpan(span) {
181
+ const spanContext = object(span.context);
182
+ const rawTraceId = spanContext?.trace_id;
183
+ const rawSpanId = spanContext?.span_id;
184
+ if (!isPhoenixSpan(span) || typeof rawTraceId !== "string" || typeof rawSpanId !== "string")
185
+ throw new ConversionError("a record that is not a Phoenix span");
186
+ const traceId = hexId(rawTraceId, 16);
187
+ const parent = text(span.parent_id);
188
+ const attributes = dictionary(span.attributes, "attributes");
189
+ const kind = text(span.span_kind);
190
+ if (kind &&
191
+ kind !== "UNKNOWN" &&
192
+ !attributes.some((item) => item.key === "openinference.span.kind"))
193
+ attributes.unshift({ key: "openinference.span.kind", value: { stringValue: kind } });
194
+ const code = text(span.status_code);
195
+ const message = text(span.status_message);
196
+ const start = instant(span.start_time, "start_time");
197
+ const end = instant(span.end_time, "end_time");
198
+ const events = span.events ?? [];
199
+ if (!Array.isArray(events))
200
+ throw new ConversionError("events is not a list");
201
+ return {
202
+ traceId,
203
+ spanKey: hexId(rawSpanId, 8),
204
+ ...(parent ? { parentKey: hexId(parent, 8) } : {}),
205
+ name: text(span.name) ?? "",
206
+ kind: 1,
207
+ ...(start !== undefined ? { start } : {}),
208
+ ...(end !== undefined ? { end } : {}),
209
+ attributes,
210
+ events: events.map((item) => {
211
+ const event = object(item);
212
+ if (!event)
213
+ throw new ConversionError("an event that is not an object");
214
+ const time = instant(event.timestamp, "an event timestamp");
215
+ return {
216
+ name: text(event.name) ?? "",
217
+ ...(time !== undefined ? { time } : {}),
218
+ attributes: dictionary(event.attributes, "event attributes"),
219
+ };
220
+ }),
221
+ links: [],
222
+ ...(code && code in STATUS
223
+ ? { status: { code: STATUS[code], ...(message ? { message } : {}) } }
224
+ : {}),
225
+ resource: [],
226
+ record: span,
227
+ };
228
+ }
229
+ function convertWith(format, records, isRecord, traceIdOf, read, context) {
230
+ const collector = context.collector(() => new OtelSpanCollector(format));
231
+ readRecords(context, records, (record) => {
232
+ const fields = object(record);
233
+ const traceId = fields ? traceIdOf(fields) : undefined;
234
+ return typeof traceId === "string" && traceId ? otelTraceKey(hexId(traceId, 16)) : undefined;
235
+ }, (record) => {
236
+ if (!isRecord(record))
237
+ throw new ConversionError(`a record that is not a ${format} span`);
238
+ collector.add(read(record), context.source.digest);
239
+ });
240
+ }
241
+ export const phoenixOtlp = {
242
+ id: "phoenix_otlp",
243
+ manifestFormat: "otlp_json",
244
+ detect: (value) => recordsOf(value, isOtlpSpan) !== undefined,
245
+ convert(value, context) {
246
+ convertWith("phoenix_otlp", recordsOf(value, isOtlpSpan) ?? [], isOtlpSpan, (record) => record.trace_id, readOtlpSpan, context);
247
+ },
248
+ };
249
+ export const phoenixSpans = {
250
+ id: "phoenix_spans",
251
+ manifestFormat: "otlp_json",
252
+ detect: (value) => recordsOf(value, isPhoenixSpan) !== undefined,
253
+ convert(value, context) {
254
+ convertWith("phoenix_spans", recordsOf(value, isPhoenixSpan) ?? [], isPhoenixSpan, (record) => object(record.context)?.trace_id, readPhoenixSpan, context);
255
+ },
256
+ };
@@ -0,0 +1,21 @@
1
+ import type { ImportAdapter } from "./adapter.js";
2
+ export declare const raindrop: ImportAdapter;
3
+ /**
4
+ * Raindrop's span rows: the Query API's `GET /v1/traces?event_id=` answer (`{"data": […]}`), one
5
+ * row alone, or one per line. Each row (`trace_id`, `span_id`, `parent_span_id`, `span_name`,
6
+ * `span_type`, `status`, `start_time_ns`, `end_time_ns`, `duration_ns`, `input`, `output`,
7
+ * `input_tokens`, `output_tokens`, `model`, `provider`, `attributes`) is one span of its trace; it
8
+ * carries the content, tool arguments and results, tokens and model an event record lacks.
9
+ *
10
+ * - `span_type` names the kind: `LLM_GENERATION` and `LLM_GENERATION_STREAM` a model call,
11
+ * `TOOL_CALL` a tool call, `INTERNAL` none. A tool span's input and output are its arguments and
12
+ * result; payload text holding JSON is read as that JSON.
13
+ * - Times are Unix nanoseconds; a row without `end_time_ns` ends `duration_ns` after it starts
14
+ * (`end_from_duration`). `status` `ERROR` is an error.
15
+ * - `attributes` (text and numbers only, as Raindrop serves them) are kept under their own names.
16
+ * The user and conversation are those Raindrop's SDKs stamp on every span of the interaction
17
+ * (`traceloop.association.properties.user_id` and `convo_id`, `ai.telemetry.metadata.raindrop.*`),
18
+ * read as Hue reads them; the rows name no event, so the event's own `user_id` is not joined in,
19
+ * and `--user-key` names a user the attributes lack.
20
+ */
21
+ export declare const raindropSpans: ImportAdapter;
@@ -0,0 +1,255 @@
1
+ import { canonicalJson, ConversionError, isObject, parseNestedJsonText, sha256Hex, unixNano, } from "../traces-import-convert.js";
2
+ import { identityText, readIdentity } from "./identity.js";
3
+ import { nanosFrom, readRecords } from "./rows.js";
4
+ const object = (value) => isObject(value) ? value : undefined;
5
+ const text = (value) => typeof value === "string" && value ? value : undefined;
6
+ /** An event as sent to the track endpoint. */
7
+ const isTrackEvent = (value) => isObject(value) &&
8
+ typeof value.event === "string" &&
9
+ (isObject(value.ai_data) || (typeof value.event_id === "string" && "user_id" in value));
10
+ /** An event as the Query API answers it. */
11
+ const isQueryEvent = (value) => isObject(value) &&
12
+ typeof value.event_name === "string" &&
13
+ "user_input" in value &&
14
+ "assistant_output" in value;
15
+ const isEvent = (value) => isObject(value) && (isTrackEvent(value) || isQueryEvent(value));
16
+ /** The events a Query API answer holds: a page (`data` a list) or one event (`data` an event). */
17
+ function envelopeEvents(value) {
18
+ if (!isObject(value))
19
+ return undefined;
20
+ if (isQueryEvent(value.data))
21
+ return [value.data];
22
+ return Array.isArray(value.data) && value.data.some(isQueryEvent) ? value.data : undefined;
23
+ }
24
+ const traceKeyOf = (record) => text(record.event_id) ?? text(record.id) ?? sha256Hex(canonicalJson(record));
25
+ /** A recorded instant, or none. */
26
+ const instant = (value) => text(value) ? BigInt(unixNano(value)) : undefined;
27
+ /** A tool call's span: its name, call, timing and status as the event records them. */
28
+ function toolRow(call, index, parent) {
29
+ const fields = object(call);
30
+ const name = text(fields?.name) ?? text(fields?.tool_name);
31
+ if (!fields || !name)
32
+ throw new ConversionError(`tool call ${index + 1} has no name`);
33
+ const start = instant(fields.started_at);
34
+ const duration = fields.duration_ms;
35
+ if (duration != null && (typeof duration !== "number" || !(duration >= 0)))
36
+ throw new ConversionError(`tool call ${index + 1} has an invalid duration_ms`);
37
+ const status = text(fields.status)?.toLowerCase();
38
+ const error = fields.error;
39
+ return {
40
+ traceKey: parent.traceKey,
41
+ spanKey: text(fields.span_id) ?? `tool_calls[${index}]`,
42
+ parentKey: parent.spanKey,
43
+ name,
44
+ kind: "tool",
45
+ ...(start !== undefined ? { start } : {}),
46
+ ...(start !== undefined && typeof duration === "number"
47
+ ? { end: start + (duration === 0 ? 0n : nanosFrom(duration, "ms")) }
48
+ : {}),
49
+ tool: {
50
+ name,
51
+ ...(text(fields.call_id) ? { callId: text(fields.call_id) } : {}),
52
+ ...(fields.arguments !== undefined ? { arguments: fields.arguments } : {}),
53
+ ...(fields.result !== undefined ? { result: fields.result } : {}),
54
+ },
55
+ ...((error !== undefined && error !== null && error !== "") || status === "error"
56
+ ? {
57
+ error: typeof error === "string" && error ? { message: error } : {},
58
+ }
59
+ : {}),
60
+ record: call,
61
+ sequence: index + 1,
62
+ };
63
+ }
64
+ function readEvent(record, context) {
65
+ const query = !isTrackEvent(record);
66
+ const ai = object(record.ai_data);
67
+ if (!query && record.ai_data != null && !ai)
68
+ throw new ConversionError("ai_data is not an object");
69
+ if (!query && !ai) {
70
+ // An analytics event: reported on its own, with no trace to refuse.
71
+ context.refuse("a Raindrop event without ai_data records no agent turn");
72
+ return;
73
+ }
74
+ const traceKey = traceKeyOf(record);
75
+ const spanKey = traceKey;
76
+ const calls = (query ? record.tool_calls : ai.tool_calls) ?? [];
77
+ if (!Array.isArray(calls))
78
+ throw new ConversionError("tool_calls is not a list");
79
+ const properties = object(record.properties);
80
+ const user = identityText(record.user_id);
81
+ const session = identityText(query ? record.convo_id : ai.convo_id);
82
+ const input = query ? record.user_input : ai.input;
83
+ const output = query ? record.assistant_output : ai.output;
84
+ const model = text(query ? record.model : ai.model);
85
+ const start = instant(record.timestamp);
86
+ const attributes = {};
87
+ const keep = (name, value) => {
88
+ if (value === undefined || value === null)
89
+ return;
90
+ if (Array.isArray(value) ? value.length : isObject(value) ? Object.keys(value).length : true)
91
+ attributes[name] = typeof value === "object" ? JSON.stringify(value) : value;
92
+ };
93
+ keep("raindrop.properties", properties);
94
+ keep("raindrop.feature_flags", record.feature_flags);
95
+ keep("raindrop.attachments", record.attachments);
96
+ keep("raindrop.topics", record.topics);
97
+ keep("raindrop.signals", record.signals);
98
+ // A pending event (`is_pending`, sent before the turn finished) gives way to the finished one.
99
+ const revision = record.is_pending === true ? "0" : "1";
100
+ const event = {
101
+ traceKey,
102
+ spanKey,
103
+ name: text(query ? record.event_name : record.event) ?? "event",
104
+ kind: "agent",
105
+ ...(start !== undefined ? { start } : {}),
106
+ ...(model ? { model } : {}),
107
+ ...(input != null && input !== "" ? { input } : {}),
108
+ ...(output != null && output !== "" ? { output } : {}),
109
+ ...(user !== undefined ? { user } : {}),
110
+ ...(session !== undefined ? { session } : {}),
111
+ attributes,
112
+ record,
113
+ sequence: 0,
114
+ };
115
+ const rows = [event, ...calls.map((call, index) => toolRow(call, index, { traceKey, spanKey }))];
116
+ for (const row of rows)
117
+ context.row({ ...row, revision });
118
+ }
119
+ export const raindrop = {
120
+ id: "raindrop",
121
+ manifestFormat: "otlp_json",
122
+ detect: (value) => isEvent(value) || envelopeEvents(value) !== undefined,
123
+ convert(value, context) {
124
+ const records = isEvent(value) ? [value] : (envelopeEvents(value) ?? []);
125
+ readRecords(context, records, (record) => (isEvent(record) ? traceKeyOf(record) : undefined), (record) => {
126
+ if (!isEvent(record))
127
+ throw new ConversionError("a record that is not a Raindrop event");
128
+ readEvent(record, context);
129
+ });
130
+ },
131
+ };
132
+ // --- raindrop_spans --------------------------------------------------------------------------
133
+ /** Raindrop's span types, by the kind each is. */
134
+ const SPAN_TYPES = {
135
+ INTERNAL: undefined,
136
+ LLM_GENERATION: "llm",
137
+ LLM_GENERATION_STREAM: "llm",
138
+ TOOL_CALL: "tool",
139
+ };
140
+ /** A span row of the Query API's `GET /v1/traces`. */
141
+ const isSpanRow = (value) => isObject(value) &&
142
+ typeof value.trace_id === "string" &&
143
+ typeof value.span_id === "string" &&
144
+ typeof value.span_name === "string" &&
145
+ typeof value.span_type === "string" &&
146
+ Object.hasOwn(SPAN_TYPES, value.span_type) &&
147
+ "start_time_ns" in value;
148
+ /** The span rows a `GET /v1/traces` answer (`{"data": […]}`) holds, when it holds any. */
149
+ const envelopeSpans = (value) => isObject(value) && Array.isArray(value.data) && value.data.some(isSpanRow)
150
+ ? value.data
151
+ : undefined;
152
+ /** A payload Raindrop keeps as text: the JSON it holds when it holds an object or a list. */
153
+ function payload(value) {
154
+ if (value === undefined || value === null || value === "")
155
+ return undefined;
156
+ if (typeof value !== "string" || !/^\s*[[{]/.test(value))
157
+ return value;
158
+ try {
159
+ return parseNestedJsonText(value).value;
160
+ }
161
+ catch {
162
+ return value;
163
+ }
164
+ }
165
+ /** An instant or duration in nanoseconds (a number, or its decimal text for one past 2^53). */
166
+ const nanos = (value) => value === undefined || value === null || value === 0 ? undefined : nanosFrom(value, "ns");
167
+ function readSpanRow(record, context) {
168
+ const traceKey = text(record.trace_id);
169
+ const spanKey = text(record.span_id);
170
+ if (!traceKey || !spanKey)
171
+ throw new ConversionError("a span without a trace_id or span_id");
172
+ const attributes = object(record.attributes);
173
+ if (record.attributes != null && !attributes)
174
+ throw new ConversionError("attributes is not an object");
175
+ const kind = SPAN_TYPES[record.span_type];
176
+ const start = nanos(record.start_time_ns);
177
+ const duration = record.end_time_ns == null ? nanos(record.duration_ns) : undefined;
178
+ const end = nanos(record.end_time_ns) ?? (start && duration ? start + duration : undefined);
179
+ const input = payload(record.input);
180
+ const output = payload(record.output);
181
+ const row = {
182
+ traceKey,
183
+ spanKey,
184
+ ...(text(record.parent_span_id) ? { parentKey: text(record.parent_span_id) } : {}),
185
+ name: text(record.span_name) ?? "span",
186
+ ...(kind ? { kind } : {}),
187
+ ...(start !== undefined ? { start } : {}),
188
+ ...(end !== undefined ? { end } : {}),
189
+ ...(record.end_time_ns == null && duration !== undefined
190
+ ? { timing: "end_from_duration" }
191
+ : {}),
192
+ ...(input !== undefined ? { input } : {}),
193
+ ...(output !== undefined ? { output } : {}),
194
+ ...(attributes ? { attributes, metadata: attributes } : {}),
195
+ record,
196
+ };
197
+ if (text(record.status) === "ERROR")
198
+ row.error = {};
199
+ if (kind === "llm") {
200
+ if (text(record.model))
201
+ row.model = text(record.model);
202
+ if (text(record.provider))
203
+ row.provider = text(record.provider).toLowerCase();
204
+ const count = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
205
+ row.usage = { input: count(record.input_tokens), output: count(record.output_tokens) };
206
+ }
207
+ if (kind === "tool")
208
+ row.tool = {
209
+ name: row.name,
210
+ ...(input !== undefined ? { arguments: input } : {}),
211
+ ...(output !== undefined ? { result: output } : {}),
212
+ };
213
+ else {
214
+ // Raindrop's SDKs stamp the interaction's user and conversation on every span they open
215
+ // (`traceloop.association.properties.user_id`, `ai.telemetry.metadata.raindrop.userId`, …),
216
+ // read as Hue reads them.
217
+ const user = readIdentity("user", (key) => attributes?.[key]);
218
+ const session = readIdentity("session", (key) => attributes?.[key]);
219
+ if (user !== undefined)
220
+ row.user = user;
221
+ if (session !== undefined)
222
+ row.session = session;
223
+ }
224
+ context.row(row);
225
+ }
226
+ /**
227
+ * Raindrop's span rows: the Query API's `GET /v1/traces?event_id=` answer (`{"data": […]}`), one
228
+ * row alone, or one per line. Each row (`trace_id`, `span_id`, `parent_span_id`, `span_name`,
229
+ * `span_type`, `status`, `start_time_ns`, `end_time_ns`, `duration_ns`, `input`, `output`,
230
+ * `input_tokens`, `output_tokens`, `model`, `provider`, `attributes`) is one span of its trace; it
231
+ * carries the content, tool arguments and results, tokens and model an event record lacks.
232
+ *
233
+ * - `span_type` names the kind: `LLM_GENERATION` and `LLM_GENERATION_STREAM` a model call,
234
+ * `TOOL_CALL` a tool call, `INTERNAL` none. A tool span's input and output are its arguments and
235
+ * result; payload text holding JSON is read as that JSON.
236
+ * - Times are Unix nanoseconds; a row without `end_time_ns` ends `duration_ns` after it starts
237
+ * (`end_from_duration`). `status` `ERROR` is an error.
238
+ * - `attributes` (text and numbers only, as Raindrop serves them) are kept under their own names.
239
+ * The user and conversation are those Raindrop's SDKs stamp on every span of the interaction
240
+ * (`traceloop.association.properties.user_id` and `convo_id`, `ai.telemetry.metadata.raindrop.*`),
241
+ * read as Hue reads them; the rows name no event, so the event's own `user_id` is not joined in,
242
+ * and `--user-key` names a user the attributes lack.
243
+ */
244
+ export const raindropSpans = {
245
+ id: "raindrop_spans",
246
+ manifestFormat: "otlp_json",
247
+ detect: (value) => isSpanRow(value) || envelopeSpans(value) !== undefined,
248
+ convert(value, context) {
249
+ readRecords(context, isSpanRow(value) ? [value] : (envelopeSpans(value) ?? []), (record) => (isSpanRow(record) ? text(record.trace_id) : undefined), (record) => {
250
+ if (!isSpanRow(record))
251
+ throw new ConversionError("a record that is not a Raindrop span");
252
+ readSpanRow(record, context);
253
+ });
254
+ },
255
+ };
@@ -0,0 +1,56 @@
1
+ import { type ConvertedTrace } from "../traces-import-convert.js";
2
+ import type { Location } from "./adapter.js";
3
+ import { type IdentityKeys } from "./identity.js";
4
+ export type { Location } from "./adapter.js";
5
+ export interface ReadResult {
6
+ /** Every trace read, in trace id order, its identity read as Hue reads it. */
7
+ traces: ConvertedTrace[];
8
+ supported: {
9
+ file: string;
10
+ format: string;
11
+ traces: number;
12
+ }[];
13
+ unsupported: {
14
+ file: string;
15
+ reason: string;
16
+ }[];
17
+ skipped: (Location & {
18
+ reason: string;
19
+ })[];
20
+ /** What was read with a repair, or read once though the input held it more than once. */
21
+ notes: (Location & {
22
+ reason: string;
23
+ })[];
24
+ refused: (Partial<Location> & {
25
+ trace_id?: string;
26
+ reason: string;
27
+ })[];
28
+ }
29
+ export declare class ImportReader {
30
+ private readonly options;
31
+ /** Run-wide collectors, by format id or by the key formats that share one name. */
32
+ private readonly collectors;
33
+ /** The shared collector key of each format that named one. */
34
+ private readonly sharedKeys;
35
+ private readonly rowCollectors;
36
+ private readonly whole;
37
+ /** Traces already refused and named; a collector's own refusal of one is not repeated. */
38
+ private readonly refusedTraces;
39
+ /** The file names each content digest was read under. */
40
+ private readonly fileNames;
41
+ private readonly supported;
42
+ private readonly result;
43
+ /** `unsupported` is where files that cannot be read are listed, in the order they are read. */
44
+ constructor(options: {
45
+ maxFileBytes: number;
46
+ keys?: IdentityKeys;
47
+ unsupported?: ReadResult["unsupported"];
48
+ });
49
+ /** Reads one file's bytes; returns the size of its decoded content. */
50
+ read(file: string, raw: Uint8Array): number;
51
+ private rows;
52
+ private convert;
53
+ /** Every trace read, merged across files: the same trace from two sources is one trace when
54
+ * its spans agree, and refused when they differ. */
55
+ finish(): ReadResult;
56
+ }