@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.
- package/CLI.md +255 -61
- package/README.md +61 -20
- package/WORLDS.md +6 -3
- package/dist/ai-sdk.d.ts +23 -1
- package/dist/ai-sdk.js +60 -2
- package/dist/cli/import-formats/adapter.d.ts +80 -0
- package/dist/cli/import-formats/adapter.js +1 -0
- package/dist/cli/import-formats/agent-events.d.ts +5 -0
- package/dist/cli/import-formats/agent-events.js +16 -0
- package/dist/cli/import-formats/atif.d.ts +3 -0
- package/dist/cli/import-formats/atif.js +15 -0
- package/dist/cli/import-formats/braintrust.d.ts +2 -0
- package/dist/cli/import-formats/braintrust.js +222 -0
- package/dist/cli/import-formats/containers.d.ts +32 -0
- package/dist/cli/import-formats/containers.js +399 -0
- package/dist/cli/import-formats/datadog.d.ts +2 -0
- package/dist/cli/import-formats/datadog.js +264 -0
- package/dist/cli/import-formats/helicone.d.ts +2 -0
- package/dist/cli/import-formats/helicone.js +161 -0
- package/dist/cli/import-formats/identity.d.ts +112 -0
- package/dist/cli/import-formats/identity.js +594 -0
- package/dist/cli/import-formats/jaeger.d.ts +3 -0
- package/dist/cli/import-formats/jaeger.js +266 -0
- package/dist/cli/import-formats/langfuse.d.ts +7 -0
- package/dist/cli/import-formats/langfuse.js +510 -0
- package/dist/cli/import-formats/langsmith.d.ts +2 -0
- package/dist/cli/import-formats/langsmith.js +244 -0
- package/dist/cli/import-formats/messages.d.ts +28 -0
- package/dist/cli/import-formats/messages.js +301 -0
- package/dist/cli/import-formats/mlflow.d.ts +2 -0
- package/dist/cli/import-formats/mlflow.js +260 -0
- package/dist/cli/import-formats/openai-agents.d.ts +2 -0
- package/dist/cli/import-formats/openai-agents.js +221 -0
- package/dist/cli/import-formats/opik.d.ts +2 -0
- package/dist/cli/import-formats/opik.js +193 -0
- package/dist/cli/import-formats/otel-family.d.ts +5 -0
- package/dist/cli/import-formats/otel-family.js +17 -0
- package/dist/cli/import-formats/otel-spans.d.ts +94 -0
- package/dist/cli/import-formats/otel-spans.js +262 -0
- package/dist/cli/import-formats/otlp-protobuf.d.ts +35 -0
- package/dist/cli/import-formats/otlp-protobuf.js +486 -0
- package/dist/cli/import-formats/otlp.d.ts +4 -0
- package/dist/cli/import-formats/otlp.js +15 -0
- package/dist/cli/import-formats/phoenix.d.ts +3 -0
- package/dist/cli/import-formats/phoenix.js +256 -0
- package/dist/cli/import-formats/raindrop.d.ts +21 -0
- package/dist/cli/import-formats/raindrop.js +255 -0
- package/dist/cli/import-formats/reader.d.ts +56 -0
- package/dist/cli/import-formats/reader.js +364 -0
- package/dist/cli/import-formats/registry.d.ts +36 -0
- package/dist/cli/import-formats/registry.js +71 -0
- package/dist/cli/import-formats/row-platform-fields.d.ts +32 -0
- package/dist/cli/import-formats/row-platform-fields.js +76 -0
- package/dist/cli/import-formats/rows.d.ts +165 -0
- package/dist/cli/import-formats/rows.js +638 -0
- package/dist/cli/import-formats/transcripts.d.ts +25 -0
- package/dist/cli/import-formats/transcripts.js +498 -0
- package/dist/cli/import-formats/weave.d.ts +2 -0
- package/dist/cli/import-formats/weave.js +159 -0
- package/dist/cli/import-formats/zipkin.d.ts +2 -0
- package/dist/cli/import-formats/zipkin.js +219 -0
- package/dist/cli/import.d.ts +1 -1
- package/dist/cli/import.js +4 -2
- package/dist/cli/mcp.d.ts +5 -1
- package/dist/cli/mcp.js +35 -23
- package/dist/cli/traces-import-convert.d.ts +65 -33
- package/dist/cli/traces-import-convert.js +211 -214
- package/dist/cli/traces-import-prepare.d.ts +26 -22
- package/dist/cli/traces-import-prepare.js +51 -122
- package/dist/cli/traces-import.d.ts +4 -4
- package/dist/cli/traces-import.js +63 -11
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { ConversionError, isObject } from "../traces-import-convert.js";
|
|
2
|
+
import { identityText } from "./identity.js";
|
|
3
|
+
import { count, jsonOf, object, text } from "./row-platform-fields.js";
|
|
4
|
+
import { nanosFrom, readRecords } from "./rows.js";
|
|
5
|
+
/**
|
|
6
|
+
* MLflow traces as `Trace.to_dict()` writes them (`mlflow traces get`, `trace.to_json()`): an
|
|
7
|
+
* `info` (`TraceInfo`: `trace_id`, `request_time`, `trace_metadata`, `tags`) and `data.spans`, each
|
|
8
|
+
* span OpenTelemetry-shaped with base64 ids, nanosecond times and every attribute JSON-encoded.
|
|
9
|
+
* Read from one trace, a JSON array or JSON Lines of them, and `mlflow traces search --output json`
|
|
10
|
+
* (`{"traces": […], "next_page_token": …}`); MLflow 2's form (`info.request_id`,
|
|
11
|
+
* `request_metadata`, spans with `context`, `parent_id` and `start_time`) is read too.
|
|
12
|
+
*
|
|
13
|
+
* - `mlflow.spanType` names the kind: `LLM` and `CHAT_MODEL` a model call, `TOOL` a tool call,
|
|
14
|
+
* `AGENT`, `RETRIEVER` and `EMBEDDING`; `mlflow.spanInputs` and `mlflow.spanOutputs` are the
|
|
15
|
+
* content, `mlflow.chat.tokenUsage` the token counts, `mlflow.llm.model` and
|
|
16
|
+
* `mlflow.llm.provider` the model.
|
|
17
|
+
* - The user and conversation are the trace's `mlflow.trace.user` and `mlflow.trace.session`
|
|
18
|
+
* metadata, else a span's own `user.id` and `session.id`. `mlflow.user` and `mlflow.auth.*`
|
|
19
|
+
* name who logged the trace, never the end user.
|
|
20
|
+
* - A trace tagged `mlflow.eval.requestId` (made by `mlflow.genai.evaluate`) or
|
|
21
|
+
* `mlflow.trace.sourceScorer` (a judge's own trace) is evaluation traffic.
|
|
22
|
+
* - A span's `links`, which `Span.to_dict()` writes beside its parent, become span links, to a
|
|
23
|
+
* span of the same trace or of another, their ids read as the trace's own are.
|
|
24
|
+
* - A trace is one record: a malformed span refuses its trace. The spans without a parent keep the
|
|
25
|
+
* trace's `info` beside the span in their source record.
|
|
26
|
+
*/
|
|
27
|
+
const isTrace = (value) => isObject(value) &&
|
|
28
|
+
isObject(value.info) &&
|
|
29
|
+
isObject(value.data) &&
|
|
30
|
+
Array.isArray(value.data.spans) &&
|
|
31
|
+
(typeof value.info.trace_id === "string" || typeof value.info.request_id === "string");
|
|
32
|
+
/** The traces `mlflow traces search --output json` answers, when it holds any. */
|
|
33
|
+
const envelopeTraces = (value) => isObject(value) && Array.isArray(value.traces) && value.traces.some(isTrace)
|
|
34
|
+
? value.traces
|
|
35
|
+
: undefined;
|
|
36
|
+
/** An MLflow id as lowercase hex: the base64 of its bytes (`Span.to_dict`), its hex (MLflow
|
|
37
|
+
* 3.5), `0x`-prefixed hex (MLflow 2) or a `tr-` trace id; undefined when it is none of these. */
|
|
38
|
+
function hexId(value, bytes) {
|
|
39
|
+
if (typeof value !== "string" || !value)
|
|
40
|
+
return undefined;
|
|
41
|
+
const hex = value.replace(/^0x|^tr-/i, "");
|
|
42
|
+
if (hex.length === bytes * 2 && /^[0-9a-f]+$/i.test(hex))
|
|
43
|
+
return hex.toLowerCase();
|
|
44
|
+
if (/^[A-Za-z0-9+/]+={0,2}$/.test(value) && value.length === Math.ceil(bytes / 3) * 4) {
|
|
45
|
+
const decoded = Buffer.from(value, "base64");
|
|
46
|
+
if (decoded.byteLength === bytes)
|
|
47
|
+
return decoded.toString("hex");
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const KINDS = {
|
|
52
|
+
LLM: "llm",
|
|
53
|
+
CHAT_MODEL: "llm",
|
|
54
|
+
TOOL: "tool",
|
|
55
|
+
AGENT: "agent",
|
|
56
|
+
RETRIEVER: "retrieval",
|
|
57
|
+
EMBEDDING: "embedding",
|
|
58
|
+
};
|
|
59
|
+
/** The span's attributes with MLflow's JSON encoding undone; text that is not JSON is kept. */
|
|
60
|
+
function decodedAttributes(value) {
|
|
61
|
+
if (value === undefined || value === null)
|
|
62
|
+
return {};
|
|
63
|
+
if (!isObject(value))
|
|
64
|
+
throw new ConversionError("a span's attributes are not an object");
|
|
65
|
+
const decoded = {};
|
|
66
|
+
for (const [key, raw] of Object.entries(value))
|
|
67
|
+
decoded[key] = jsonOf(raw) ?? raw;
|
|
68
|
+
return decoded;
|
|
69
|
+
}
|
|
70
|
+
/** An attribute value OTLP holds as it is: text, a number or a boolean; anything else as JSON. */
|
|
71
|
+
const attributeValue = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean"
|
|
72
|
+
? value
|
|
73
|
+
: JSON.stringify(value);
|
|
74
|
+
/** A trace reference as a trace key: an id in any of MLflow's forms (`tr-<hex>`, hex, a v4
|
|
75
|
+
* `trace:/<location>/<hex>`), else the reference as recorded. */
|
|
76
|
+
const traceKeyOf = (reference) => hexId(reference, 16) ?? hexId(reference.split("/").at(-1), 16) ?? reference;
|
|
77
|
+
/** A span's links (`Link.to_dict()`: `trace_id`, `span_id`, `attributes`). */
|
|
78
|
+
function linksOf(value) {
|
|
79
|
+
if (value === undefined || value === null)
|
|
80
|
+
return [];
|
|
81
|
+
if (!Array.isArray(value))
|
|
82
|
+
throw new ConversionError("a span's links are not a list");
|
|
83
|
+
return value.map((item) => {
|
|
84
|
+
const link = object(item);
|
|
85
|
+
const traceId = text(link?.trace_id);
|
|
86
|
+
const spanId = text(link?.span_id);
|
|
87
|
+
if (!link || !traceId || !spanId)
|
|
88
|
+
throw new ConversionError("a span link without a trace_id or span_id");
|
|
89
|
+
const attributes = object(link.attributes);
|
|
90
|
+
return {
|
|
91
|
+
traceKey: traceKeyOf(traceId),
|
|
92
|
+
spanKey: hexId(spanId, 8) ?? spanId,
|
|
93
|
+
...(attributes ? { attributes } : {}),
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** The instant a span field records, in nanoseconds; undefined when absent. */
|
|
98
|
+
const instant = (value) => value === undefined || value === null || value === "" || value === 0
|
|
99
|
+
? undefined
|
|
100
|
+
: nanosFrom(value, "ns");
|
|
101
|
+
/** One span as a row and its events. */
|
|
102
|
+
function readSpan(span, trace) {
|
|
103
|
+
if (!isObject(span))
|
|
104
|
+
throw new ConversionError("a span that is not an object");
|
|
105
|
+
const v2 = isObject(span.context);
|
|
106
|
+
const rawId = v2 ? span.context.span_id : span.span_id;
|
|
107
|
+
const rawParent = v2 ? span.parent_id : span.parent_span_id;
|
|
108
|
+
const spanKey = hexId(rawId, 8) ?? text(rawId);
|
|
109
|
+
if (spanKey === undefined)
|
|
110
|
+
throw new ConversionError("a span without a span_id");
|
|
111
|
+
const parentKey = rawParent ? (hexId(rawParent, 8) ?? text(rawParent)) : undefined;
|
|
112
|
+
const status = object(span.status);
|
|
113
|
+
const code = text(v2 ? span.status_code : status?.code);
|
|
114
|
+
const message = text(v2 ? span.status_message : status?.message);
|
|
115
|
+
const attributes = decodedAttributes(span.attributes);
|
|
116
|
+
const type = text(attributes["mlflow.spanType"]);
|
|
117
|
+
const kind = type && Object.hasOwn(KINDS, type) ? KINDS[type] : undefined;
|
|
118
|
+
const start = instant(v2 ? span.start_time : span.start_time_unix_nano);
|
|
119
|
+
const end = instant(v2 ? span.end_time : span.end_time_unix_nano);
|
|
120
|
+
const events = span.events ?? [];
|
|
121
|
+
if (!Array.isArray(events))
|
|
122
|
+
throw new ConversionError("a span's events are not a list");
|
|
123
|
+
let exceptionType;
|
|
124
|
+
const eventRows = events.map((event) => {
|
|
125
|
+
if (!isObject(event))
|
|
126
|
+
throw new ConversionError("a span event that is not an object");
|
|
127
|
+
const time = instant(v2 ? event.timestamp : event.time_unix_nano);
|
|
128
|
+
const eventAttributes = object(event.attributes);
|
|
129
|
+
if (event.name === "exception")
|
|
130
|
+
exceptionType ??= text(eventAttributes?.["exception.type"]);
|
|
131
|
+
return {
|
|
132
|
+
traceKey: trace.traceKey,
|
|
133
|
+
spanKey,
|
|
134
|
+
name: text(event.name) ?? "event",
|
|
135
|
+
...(time !== undefined ? { time } : {}),
|
|
136
|
+
...(eventAttributes ? { attributes: eventAttributes } : {}),
|
|
137
|
+
record: event,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
const links = linksOf(span.links);
|
|
141
|
+
const input = attributes["mlflow.spanInputs"];
|
|
142
|
+
const output = attributes["mlflow.spanOutputs"];
|
|
143
|
+
const own = {};
|
|
144
|
+
for (const [key, value] of Object.entries(attributes))
|
|
145
|
+
if (key.startsWith("mlflow.") &&
|
|
146
|
+
key !== "mlflow.spanInputs" &&
|
|
147
|
+
key !== "mlflow.spanOutputs" &&
|
|
148
|
+
value !== null)
|
|
149
|
+
own[key] = attributeValue(value);
|
|
150
|
+
const row = {
|
|
151
|
+
traceKey: trace.traceKey,
|
|
152
|
+
spanKey,
|
|
153
|
+
...(parentKey !== undefined ? { parentKey } : {}),
|
|
154
|
+
...(links.length ? { links } : {}),
|
|
155
|
+
name: text(span.name) ?? "span",
|
|
156
|
+
...(kind ? { kind } : {}),
|
|
157
|
+
...(start !== undefined ? { start } : {}),
|
|
158
|
+
...(end !== undefined ? { end } : {}),
|
|
159
|
+
...(input !== undefined && input !== null ? { input } : {}),
|
|
160
|
+
...(output !== undefined && output !== null ? { output } : {}),
|
|
161
|
+
record: parentKey === undefined ? { info: trace.info, span } : span,
|
|
162
|
+
...(trace.metadata ? { metadata: trace.metadata } : {}),
|
|
163
|
+
attributes: own,
|
|
164
|
+
};
|
|
165
|
+
if (code && /ERROR$/.test(code))
|
|
166
|
+
row.error = {
|
|
167
|
+
...(message ? { message } : {}),
|
|
168
|
+
...(exceptionType ? { type: exceptionType } : {}),
|
|
169
|
+
};
|
|
170
|
+
const model = text(attributes["mlflow.llm.model"]);
|
|
171
|
+
const provider = text(attributes["mlflow.llm.provider"]);
|
|
172
|
+
if (model)
|
|
173
|
+
row.model = model;
|
|
174
|
+
if (provider)
|
|
175
|
+
row.provider = provider;
|
|
176
|
+
const usage = object(attributes["mlflow.chat.tokenUsage"]);
|
|
177
|
+
if (usage)
|
|
178
|
+
row.usage = {
|
|
179
|
+
input: count(usage.input_tokens),
|
|
180
|
+
output: count(usage.output_tokens),
|
|
181
|
+
cacheRead: count(usage.cache_read_input_tokens),
|
|
182
|
+
cacheWrite: count(usage.cache_creation_input_tokens),
|
|
183
|
+
};
|
|
184
|
+
if (kind === "tool")
|
|
185
|
+
row.tool = {
|
|
186
|
+
name: row.name,
|
|
187
|
+
...(input !== undefined && input !== null ? { arguments: input } : {}),
|
|
188
|
+
...(output !== undefined && output !== null ? { result: output } : {}),
|
|
189
|
+
};
|
|
190
|
+
else {
|
|
191
|
+
const user = trace.user ?? identityText(attributes["user.id"]);
|
|
192
|
+
const session = trace.session ?? identityText(attributes["session.id"]);
|
|
193
|
+
if (user !== undefined)
|
|
194
|
+
row.user = user;
|
|
195
|
+
if (session !== undefined)
|
|
196
|
+
row.session = session;
|
|
197
|
+
}
|
|
198
|
+
if (trace.evaluation)
|
|
199
|
+
row.origin = "evaluation";
|
|
200
|
+
return { row, events: eventRows };
|
|
201
|
+
}
|
|
202
|
+
function readTrace(value, context) {
|
|
203
|
+
const trace = traceOf(value);
|
|
204
|
+
if (!trace)
|
|
205
|
+
throw new ConversionError("a trace without a trace_id");
|
|
206
|
+
const spans = value.data.spans;
|
|
207
|
+
if (!spans.length)
|
|
208
|
+
throw new ConversionError("a trace exported without its spans; export it with its spans");
|
|
209
|
+
// Every span is read before any is added, so a malformed one refuses the trace whole.
|
|
210
|
+
const read = spans.map((span) => readSpan(span, trace));
|
|
211
|
+
for (const { row, events } of read) {
|
|
212
|
+
context.row(row);
|
|
213
|
+
for (const event of events)
|
|
214
|
+
context.event(event);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function traceOf(value) {
|
|
218
|
+
const info = value.info;
|
|
219
|
+
const reference = text(info.trace_id) ?? text(info.request_id);
|
|
220
|
+
if (reference === undefined)
|
|
221
|
+
return undefined;
|
|
222
|
+
const spans = value.data.spans;
|
|
223
|
+
const spanTraces = new Set(spans.flatMap((span) => {
|
|
224
|
+
const raw = isObject(span)
|
|
225
|
+
? isObject(span.context)
|
|
226
|
+
? span.context.trace_id
|
|
227
|
+
: span.trace_id
|
|
228
|
+
: undefined;
|
|
229
|
+
return hexId(raw, 16) ?? [];
|
|
230
|
+
}));
|
|
231
|
+
const traceKey = hexId(reference, 16) ??
|
|
232
|
+
hexId(reference.split("/").at(-1), 16) ??
|
|
233
|
+
(spanTraces.size === 1 ? [...spanTraces][0] : traceKeyOf(reference));
|
|
234
|
+
const metadata = object(info.trace_metadata ?? info.request_metadata);
|
|
235
|
+
const tags = object(info.tags);
|
|
236
|
+
const user = identityText(metadata?.["mlflow.trace.user"]);
|
|
237
|
+
const session = identityText(metadata?.["mlflow.trace.session"]);
|
|
238
|
+
return {
|
|
239
|
+
traceKey,
|
|
240
|
+
info,
|
|
241
|
+
...(metadata ? { metadata } : {}),
|
|
242
|
+
...(user !== undefined ? { user } : {}),
|
|
243
|
+
...(session !== undefined ? { session } : {}),
|
|
244
|
+
evaluation: tags?.["mlflow.eval.requestId"] !== undefined ||
|
|
245
|
+
tags?.["mlflow.trace.sourceScorer"] !== undefined,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
export const mlflowTraces = {
|
|
249
|
+
id: "mlflow_traces",
|
|
250
|
+
manifestFormat: "otlp_json",
|
|
251
|
+
detect: (value) => isTrace(value) || envelopeTraces(value) !== undefined,
|
|
252
|
+
convert(value, context) {
|
|
253
|
+
const records = isTrace(value) ? [value] : (envelopeTraces(value) ?? []);
|
|
254
|
+
readRecords(context, records, (record) => (isTrace(record) ? traceOf(record)?.traceKey : undefined), (record) => {
|
|
255
|
+
if (!isTrace(record))
|
|
256
|
+
throw new ConversionError("a record that is not an MLflow trace");
|
|
257
|
+
readTrace(record, context);
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { ConversionError, isObject, parseNestedJsonText, unixNano, } from "../traces-import-convert.js";
|
|
2
|
+
import { identityText } from "./identity.js";
|
|
3
|
+
import { metadataUser } from "./row-platform-fields.js";
|
|
4
|
+
import { readRecords } from "./rows.js";
|
|
5
|
+
const object = (value) => isObject(value) ? value : undefined;
|
|
6
|
+
const text = (value) => typeof value === "string" && value ? value : undefined;
|
|
7
|
+
const count = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
8
|
+
const strings = (value) => Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
9
|
+
? value
|
|
10
|
+
: undefined;
|
|
11
|
+
const isTrace = (value) => isObject(value) && value.object === "trace" && typeof value.id === "string";
|
|
12
|
+
const isSpan = (value) => isObject(value) && value.object === "trace.span" && typeof value.trace_id === "string";
|
|
13
|
+
const isRecord = (value) => isTrace(value) || isSpan(value);
|
|
14
|
+
/** The records an ingest body (`{"data": […]}`) holds, when it holds any. */
|
|
15
|
+
function envelopeRecords(value) {
|
|
16
|
+
return isObject(value) && Array.isArray(value.data) && value.data.some(isRecord)
|
|
17
|
+
? value.data
|
|
18
|
+
: undefined;
|
|
19
|
+
}
|
|
20
|
+
const SESSION_FIELDS = [
|
|
21
|
+
"session_id",
|
|
22
|
+
"sessionId",
|
|
23
|
+
"thread_id",
|
|
24
|
+
"threadId",
|
|
25
|
+
"conversation_id",
|
|
26
|
+
"conversationId",
|
|
27
|
+
];
|
|
28
|
+
const first = (fields, names) => {
|
|
29
|
+
for (const name of names) {
|
|
30
|
+
const value = identityText(fields?.[name]);
|
|
31
|
+
if (value !== undefined)
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
};
|
|
36
|
+
/** An instant the SDK recorded (`started_at`, `ended_at`), or none when it is null. */
|
|
37
|
+
const instant = (value) => value === undefined || value === null ? undefined : BigInt(unixNano(value));
|
|
38
|
+
/** Token usage of a model call: `input_tokens`, `output_tokens` and their details, also as the
|
|
39
|
+
* SDK's OpenAI exporter nests them under `details`. */
|
|
40
|
+
function usageOf(value) {
|
|
41
|
+
const usage = object(value);
|
|
42
|
+
if (!usage)
|
|
43
|
+
return undefined;
|
|
44
|
+
const details = object(usage.details);
|
|
45
|
+
const detail = (name) => {
|
|
46
|
+
const found = usage[name] ?? details?.[name];
|
|
47
|
+
return object(Array.isArray(found) ? found[0] : found);
|
|
48
|
+
};
|
|
49
|
+
const input = detail("input_tokens_details");
|
|
50
|
+
const output = detail("output_tokens_details");
|
|
51
|
+
return {
|
|
52
|
+
input: count(usage.input_tokens),
|
|
53
|
+
output: count(usage.output_tokens),
|
|
54
|
+
cacheRead: count(input?.cached_tokens) ?? count(usage.cached_input_tokens),
|
|
55
|
+
cacheWrite: count(input?.cache_write_tokens) ?? count(usage.cache_write_input_tokens),
|
|
56
|
+
reasoning: count(output?.reasoning_tokens),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** A function tool's arguments: the JSON text the model wrote, parsed when it is JSON. */
|
|
60
|
+
function argumentsOf(value) {
|
|
61
|
+
if (typeof value !== "string")
|
|
62
|
+
return value ?? undefined;
|
|
63
|
+
try {
|
|
64
|
+
return parseNestedJsonText(value).value;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function readTrace(record, context) {
|
|
71
|
+
const traceKey = text(record.id);
|
|
72
|
+
if (!traceKey)
|
|
73
|
+
throw new ConversionError("a trace without an id");
|
|
74
|
+
const metadata = object(record.metadata);
|
|
75
|
+
if (record.metadata != null && !metadata)
|
|
76
|
+
throw new ConversionError("metadata is not an object");
|
|
77
|
+
const user = metadataUser(metadata);
|
|
78
|
+
const session = identityText(record.group_id) ?? first(metadata, SESSION_FIELDS);
|
|
79
|
+
context.row({
|
|
80
|
+
traceKey,
|
|
81
|
+
spanKey: traceKey,
|
|
82
|
+
name: text(record.workflow_name) ?? "trace",
|
|
83
|
+
...(user !== undefined ? { user } : {}),
|
|
84
|
+
...(session !== undefined ? { session } : {}),
|
|
85
|
+
attributes: metadata && Object.keys(metadata).length
|
|
86
|
+
? { "openai_agents.metadata": JSON.stringify(metadata) }
|
|
87
|
+
: {},
|
|
88
|
+
record,
|
|
89
|
+
...(metadata ? { metadata } : {}),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function readSpan(record, context) {
|
|
93
|
+
const traceKey = text(record.trace_id);
|
|
94
|
+
const spanKey = text(record.id);
|
|
95
|
+
if (!traceKey || !spanKey)
|
|
96
|
+
throw new ConversionError("a span without an id or trace_id");
|
|
97
|
+
if (record.parent_id != null && !text(record.parent_id))
|
|
98
|
+
throw new ConversionError("parent_id is not a span id");
|
|
99
|
+
const data = object(record.span_data);
|
|
100
|
+
const type = text(data?.type);
|
|
101
|
+
if (!data || !type)
|
|
102
|
+
throw new ConversionError("span_data has no type");
|
|
103
|
+
const error = record.error;
|
|
104
|
+
if (error != null && !object(error))
|
|
105
|
+
throw new ConversionError("error is not an object");
|
|
106
|
+
const metadata = object(record.metadata);
|
|
107
|
+
const name = text(data.name) ?? type;
|
|
108
|
+
const attributes = { "openai_agents.span.type": type };
|
|
109
|
+
const json = (key, value) => {
|
|
110
|
+
if (value !== undefined && value !== null)
|
|
111
|
+
attributes[key] = JSON.stringify(value);
|
|
112
|
+
};
|
|
113
|
+
if (metadata && Object.keys(metadata).length)
|
|
114
|
+
json("openai_agents.metadata", metadata);
|
|
115
|
+
let kind;
|
|
116
|
+
const row = {};
|
|
117
|
+
switch (type) {
|
|
118
|
+
case "agent":
|
|
119
|
+
kind = "agent";
|
|
120
|
+
row.agentName = name;
|
|
121
|
+
for (const field of ["handoffs", "tools"])
|
|
122
|
+
if (strings(data[field]))
|
|
123
|
+
attributes[`openai_agents.agent.${field}`] = strings(data[field]);
|
|
124
|
+
if (text(data.output_type))
|
|
125
|
+
attributes["openai_agents.agent.output_type"] = data.output_type;
|
|
126
|
+
break;
|
|
127
|
+
case "function":
|
|
128
|
+
kind = "tool";
|
|
129
|
+
row.tool = {
|
|
130
|
+
name,
|
|
131
|
+
...(data.input != null ? { arguments: argumentsOf(data.input) } : {}),
|
|
132
|
+
...(data.output != null ? { result: data.output } : {}),
|
|
133
|
+
};
|
|
134
|
+
json("openai_agents.mcp_data", data.mcp_data);
|
|
135
|
+
break;
|
|
136
|
+
case "generation":
|
|
137
|
+
kind = "llm";
|
|
138
|
+
if (data.input != null)
|
|
139
|
+
row.input = data.input;
|
|
140
|
+
if (data.output != null)
|
|
141
|
+
row.output = data.output;
|
|
142
|
+
if (text(data.model))
|
|
143
|
+
row.model = text(data.model);
|
|
144
|
+
row.usage = usageOf(data.usage);
|
|
145
|
+
json("openai_agents.model_config", data.model_config);
|
|
146
|
+
break;
|
|
147
|
+
case "response":
|
|
148
|
+
kind = "llm";
|
|
149
|
+
if (text(data.response_id))
|
|
150
|
+
attributes["gen_ai.response.id"] = data.response_id;
|
|
151
|
+
row.usage = usageOf(data.usage);
|
|
152
|
+
break;
|
|
153
|
+
case "handoff":
|
|
154
|
+
if (text(data.from_agent))
|
|
155
|
+
attributes["openai_agents.handoff.from_agent"] = data.from_agent;
|
|
156
|
+
if (text(data.to_agent))
|
|
157
|
+
attributes["openai_agents.handoff.to_agent"] = data.to_agent;
|
|
158
|
+
break;
|
|
159
|
+
case "guardrail":
|
|
160
|
+
if (typeof data.triggered === "boolean")
|
|
161
|
+
attributes["openai_agents.guardrail.triggered"] = data.triggered;
|
|
162
|
+
break;
|
|
163
|
+
case "custom":
|
|
164
|
+
json("openai_agents.custom.data", data.data);
|
|
165
|
+
break;
|
|
166
|
+
case "mcp_tools":
|
|
167
|
+
if (text(data.server))
|
|
168
|
+
attributes["openai_agents.mcp.server"] = data.server;
|
|
169
|
+
if (strings(data.result))
|
|
170
|
+
attributes["openai_agents.mcp.tools"] = strings(data.result);
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
// Voice and future span types: the model they name, their data in the source record.
|
|
174
|
+
if (text(data.model))
|
|
175
|
+
row.model = text(data.model);
|
|
176
|
+
}
|
|
177
|
+
const start = instant(record.started_at);
|
|
178
|
+
const end = instant(record.ended_at);
|
|
179
|
+
const errorFields = object(error);
|
|
180
|
+
context.row({
|
|
181
|
+
traceKey,
|
|
182
|
+
spanKey,
|
|
183
|
+
parentKey: text(record.parent_id) ?? traceKey,
|
|
184
|
+
name,
|
|
185
|
+
...(kind ? { kind } : {}),
|
|
186
|
+
...(start !== undefined ? { start } : {}),
|
|
187
|
+
...(end !== undefined ? { end } : {}),
|
|
188
|
+
...row,
|
|
189
|
+
...(errorFields
|
|
190
|
+
? {
|
|
191
|
+
error: text(errorFields.message) ? { message: text(errorFields.message) } : {},
|
|
192
|
+
}
|
|
193
|
+
: {}),
|
|
194
|
+
attributes: {
|
|
195
|
+
...attributes,
|
|
196
|
+
...(errorFields?.data != null
|
|
197
|
+
? { "openai_agents.error.data": JSON.stringify(errorFields.data) }
|
|
198
|
+
: {}),
|
|
199
|
+
},
|
|
200
|
+
record,
|
|
201
|
+
...(metadata ? { metadata } : {}),
|
|
202
|
+
// A processor may write a span when it starts and again when it ends: the ended copy wins.
|
|
203
|
+
revision: end === undefined ? "0" : "1",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
export const openaiAgents = {
|
|
207
|
+
id: "openai_agents",
|
|
208
|
+
manifestFormat: "otlp_json",
|
|
209
|
+
detect: (value) => isRecord(value) || envelopeRecords(value) !== undefined,
|
|
210
|
+
convert(value, context) {
|
|
211
|
+
const records = isRecord(value) ? [value] : (envelopeRecords(value) ?? []);
|
|
212
|
+
readRecords(context, records, (record) => isTrace(record) ? text(record.id) : isSpan(record) ? text(record.trace_id) : undefined, (record) => {
|
|
213
|
+
if (isTrace(record))
|
|
214
|
+
readTrace(record, context);
|
|
215
|
+
else if (isSpan(record))
|
|
216
|
+
readSpan(record, context);
|
|
217
|
+
else
|
|
218
|
+
throw new ConversionError("a record that is not an OpenAI Agents trace or span");
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { ConversionError, isObject } from "../traces-import-convert.js";
|
|
2
|
+
import { identityText } from "./identity.js";
|
|
3
|
+
import { count, first, isoNanos, metadataUser, object, SESSION_FIELDS, text, } from "./row-platform-fields.js";
|
|
4
|
+
import { readRecords } from "./rows.js";
|
|
5
|
+
/**
|
|
6
|
+
* Opik traces and spans (`TracePublic` and `SpanPublic` in Opik's REST API). Read from the files
|
|
7
|
+
* `opik export … project …` writes, one per trace (`{"trace": …, "spans": […], …}`), and from
|
|
8
|
+
* span records alone: one span, JSON Lines of them or the spans API's page (`{"content": […]}`).
|
|
9
|
+
*
|
|
10
|
+
* - The trace becomes the root span of its trace, its id the span id; a span without a
|
|
11
|
+
* `parent_span_id` is its child. Spans read without their trace keep it as their missing parent.
|
|
12
|
+
* - `type` names the kind: `llm` a model call, `tool` a tool call; `general` and `guardrail` record
|
|
13
|
+
* no operation. Token counts (`usage`) are read from `llm` spans, `prompt_tokens` and
|
|
14
|
+
* `completion_tokens` with the provider's own cache and reasoning counts (`original_usage.*`).
|
|
15
|
+
* - The conversation is the trace's `thread_id`, else its metadata's; the user comes from the
|
|
16
|
+
* metadata (`userId`, `user_id`, …, in the order Hue reads a metadata map). `created_by` names
|
|
17
|
+
* who logged it, never the end user.
|
|
18
|
+
* - A trace from an experiment, evaluator, optimization or the playground (`source`, `experiment`)
|
|
19
|
+
* is evaluation traffic; `environment` is the deployment environment.
|
|
20
|
+
* - A record exported more than once keeps the copy updated last (`last_updated_at`).
|
|
21
|
+
*/
|
|
22
|
+
const SPAN_TYPES = new Set(["general", "tool", "llm", "guardrail"]);
|
|
23
|
+
const KINDS = { llm: "llm", tool: "tool" };
|
|
24
|
+
const EVALUATION_SOURCES = new Set(["experiment", "evaluator", "optimization", "playground"]);
|
|
25
|
+
const isSpan = (value) => isObject(value) &&
|
|
26
|
+
typeof value.id === "string" &&
|
|
27
|
+
typeof value.trace_id === "string" &&
|
|
28
|
+
typeof value.type === "string" &&
|
|
29
|
+
SPAN_TYPES.has(value.type) &&
|
|
30
|
+
typeof value.start_time === "string" &&
|
|
31
|
+
value.run_type === undefined;
|
|
32
|
+
/** An `opik export` trace file: the trace with its spans. */
|
|
33
|
+
const isExport = (value) => isObject(value) &&
|
|
34
|
+
isObject(value.trace) &&
|
|
35
|
+
typeof value.trace.id === "string" &&
|
|
36
|
+
typeof value.trace.start_time === "string" &&
|
|
37
|
+
Array.isArray(value.spans) &&
|
|
38
|
+
(value.spans.some(isSpan) || typeof value.downloaded_at === "string");
|
|
39
|
+
/** The spans a spans API page holds, when it holds any. */
|
|
40
|
+
const spanPage = (value) => isObject(value) && Array.isArray(value.content) && value.content.some(isSpan)
|
|
41
|
+
? value.content
|
|
42
|
+
: undefined;
|
|
43
|
+
/** A token count from Opik's usage, the OpenAI-shaped total first, then the provider's own. */
|
|
44
|
+
const usageCount = (usage, names) => {
|
|
45
|
+
for (const name of names) {
|
|
46
|
+
const value = count(usage?.[name]);
|
|
47
|
+
if (value !== undefined)
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
};
|
|
52
|
+
/** The fields Opik's trace and span records share, as a row. */
|
|
53
|
+
function readRecord(record, traceKey, spanKey, parentKey) {
|
|
54
|
+
const start = isoNanos(record.start_time);
|
|
55
|
+
const end = record.end_time == null ? undefined : isoNanos(record.end_time);
|
|
56
|
+
const metadata = object(record.metadata);
|
|
57
|
+
const tags = Array.isArray(record.tags) && record.tags.every((tag) => typeof tag === "string")
|
|
58
|
+
? record.tags
|
|
59
|
+
: undefined;
|
|
60
|
+
const updated = record.last_updated_at == null ? undefined : isoNanos(record.last_updated_at);
|
|
61
|
+
const row = {
|
|
62
|
+
traceKey,
|
|
63
|
+
spanKey,
|
|
64
|
+
...(parentKey !== undefined ? { parentKey } : {}),
|
|
65
|
+
name: text(record.name) ?? "span",
|
|
66
|
+
start,
|
|
67
|
+
...(end !== undefined ? { end } : {}),
|
|
68
|
+
...(record.input != null ? { input: record.input } : {}),
|
|
69
|
+
...(record.output != null ? { output: record.output } : {}),
|
|
70
|
+
record,
|
|
71
|
+
...(metadata ? { metadata } : {}),
|
|
72
|
+
...(updated !== undefined ? { revision: updated.toString() } : {}),
|
|
73
|
+
attributes: {
|
|
74
|
+
...(record.metadata != null
|
|
75
|
+
? {
|
|
76
|
+
"opik.metadata": typeof record.metadata === "string"
|
|
77
|
+
? record.metadata
|
|
78
|
+
: JSON.stringify(record.metadata),
|
|
79
|
+
}
|
|
80
|
+
: {}),
|
|
81
|
+
...(tags?.length ? { "opik.tags": tags } : {}),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const error = object(record.error_info);
|
|
85
|
+
if (record.error_info != null && !error)
|
|
86
|
+
throw new ConversionError("error_info is not an object");
|
|
87
|
+
if (error) {
|
|
88
|
+
const type = text(error.exception_type);
|
|
89
|
+
const message = text(error.message) ?? type;
|
|
90
|
+
row.error = { ...(message ? { message } : {}), ...(type ? { type } : {}) };
|
|
91
|
+
}
|
|
92
|
+
const source = text(record.source);
|
|
93
|
+
if ((source && EVALUATION_SOURCES.has(source)) || isObject(record.experiment))
|
|
94
|
+
row.origin = "evaluation";
|
|
95
|
+
const environment = text(record.environment);
|
|
96
|
+
if (environment)
|
|
97
|
+
row.environment = environment;
|
|
98
|
+
return row;
|
|
99
|
+
}
|
|
100
|
+
function traceRow(trace) {
|
|
101
|
+
const id = trace.id;
|
|
102
|
+
const row = readRecord(trace, id, id, undefined);
|
|
103
|
+
const metadata = object(trace.metadata);
|
|
104
|
+
const user = metadataUser(metadata);
|
|
105
|
+
const session = identityText(trace.thread_id) ?? first(metadata, SESSION_FIELDS);
|
|
106
|
+
if (user !== undefined)
|
|
107
|
+
row.user = user;
|
|
108
|
+
if (session !== undefined)
|
|
109
|
+
row.session = session;
|
|
110
|
+
return row;
|
|
111
|
+
}
|
|
112
|
+
function spanRow(span) {
|
|
113
|
+
const traceKey = text(span.trace_id);
|
|
114
|
+
const spanKey = text(span.id);
|
|
115
|
+
if (!traceKey || !spanKey)
|
|
116
|
+
throw new ConversionError("a span without an id or trace_id");
|
|
117
|
+
const parent = text(span.parent_span_id) ?? traceKey;
|
|
118
|
+
const row = readRecord(span, traceKey, spanKey, parent);
|
|
119
|
+
const type = text(span.type);
|
|
120
|
+
const kind = type && Object.hasOwn(KINDS, type) ? KINDS[type] : undefined;
|
|
121
|
+
if (kind)
|
|
122
|
+
row.kind = kind;
|
|
123
|
+
if (type)
|
|
124
|
+
row.attributes["opik.span.type"] = type;
|
|
125
|
+
const model = text(span.model);
|
|
126
|
+
const provider = text(span.provider);
|
|
127
|
+
if (model)
|
|
128
|
+
row.model = model;
|
|
129
|
+
if (provider)
|
|
130
|
+
row.provider = provider;
|
|
131
|
+
const usage = object(span.usage);
|
|
132
|
+
if (kind === "llm" && usage)
|
|
133
|
+
row.usage = {
|
|
134
|
+
input: usageCount(usage, ["prompt_tokens", "original_usage.input_tokens"]),
|
|
135
|
+
output: usageCount(usage, ["completion_tokens", "original_usage.output_tokens"]),
|
|
136
|
+
cacheRead: usageCount(usage, [
|
|
137
|
+
"original_usage.prompt_tokens_details.cached_tokens",
|
|
138
|
+
"original_usage.input_tokens_details.cached_tokens",
|
|
139
|
+
"original_usage.cache_read_input_tokens",
|
|
140
|
+
]),
|
|
141
|
+
cacheWrite: usageCount(usage, ["original_usage.cache_creation_input_tokens"]),
|
|
142
|
+
reasoning: usageCount(usage, [
|
|
143
|
+
"original_usage.completion_tokens_details.reasoning_tokens",
|
|
144
|
+
"original_usage.output_tokens_details.reasoning_tokens",
|
|
145
|
+
]),
|
|
146
|
+
};
|
|
147
|
+
if (kind === "tool")
|
|
148
|
+
row.tool = {
|
|
149
|
+
name: row.name,
|
|
150
|
+
...(row.input !== undefined ? { arguments: row.input } : {}),
|
|
151
|
+
...(row.output !== undefined ? { result: row.output } : {}),
|
|
152
|
+
};
|
|
153
|
+
else {
|
|
154
|
+
const metadata = object(span.metadata);
|
|
155
|
+
const user = metadataUser(metadata);
|
|
156
|
+
const session = first(metadata, SESSION_FIELDS);
|
|
157
|
+
if (user !== undefined)
|
|
158
|
+
row.user = user;
|
|
159
|
+
if (session !== undefined)
|
|
160
|
+
row.session = session;
|
|
161
|
+
}
|
|
162
|
+
return row;
|
|
163
|
+
}
|
|
164
|
+
/** Reads an export file whole: a malformed trace or span refuses the trace. */
|
|
165
|
+
function readExport(value, context) {
|
|
166
|
+
const traceKey = value.trace.id;
|
|
167
|
+
const rows = [traceRow(value.trace)];
|
|
168
|
+
for (const span of value.spans) {
|
|
169
|
+
if (!isObject(span))
|
|
170
|
+
throw new ConversionError("a span that is not an object");
|
|
171
|
+
if (span.trace_id !== traceKey)
|
|
172
|
+
throw new ConversionError("a span of another trace");
|
|
173
|
+
rows.push(spanRow(span));
|
|
174
|
+
}
|
|
175
|
+
for (const row of rows)
|
|
176
|
+
context.row(row);
|
|
177
|
+
}
|
|
178
|
+
export const opikTraces = {
|
|
179
|
+
id: "opik_traces",
|
|
180
|
+
manifestFormat: "otlp_json",
|
|
181
|
+
detect: (value) => isExport(value) || isSpan(value) || spanPage(value) !== undefined,
|
|
182
|
+
convert(value, context) {
|
|
183
|
+
if (isExport(value)) {
|
|
184
|
+
readRecords(context, [value], (record) => record.trace.id, (record) => readExport(record, context));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
readRecords(context, isSpan(value) ? [value] : (spanPage(value) ?? []), (record) => (isObject(record) ? text(record.trace_id) : undefined), (record) => {
|
|
188
|
+
if (!isObject(record))
|
|
189
|
+
throw new ConversionError("a record that is not an Opik span");
|
|
190
|
+
context.row(spanRow(record));
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
};
|